互递的一般语法为:
command ::= ... | mutual declaration* end
其中各声明必须是定义或定理。
允许任意递归函数定义会使 Lean 的逻辑不一致。一般递归使得可以写出环形证明:“命题 P
为真,因为命题 P
为真”。在证明之外,一个无限循环可以被赋予类型 Empty
,再结合 Lean.Parser.Term.nomatch : term
Empty match/ex falso. `nomatch e` is of arbitrary type `α : Sort u` if
Lean can show that an empty set of patterns is exhaustive given `e`'s type,
e.g. because it has no constructors.
nomatch
或 Empty.rec
,即可“证明”任意定理。
直接禁止递归函数定义将大幅降低 Lean 的实用性:归纳类型 是定义谓词与数据的关键,而它们本身具有递归结构。此外,多数有用的递归函数并不威胁自洽性,而无限循环通常意味着定义有误而非有意为之。Lean 并未一禁了之,而是要求每个递归函数都以安全的方式定义。在繁释递归定义的过程中,Lean 的繁释器还会同时给出该定义安全的理由。可参阅繁释概览中的 繁释器的输出 一节,了解递归定义繁释在整体繁释流程中的位置。
可以定义的递归函数主要有五类:
结构化递归函数接收某个实参,并且仅在该实参的真子项上进行递归调用。严格来说,类型为 索引族 的实参会与其索引成组,把整个集合视作一个整体。 繁释器会把递归翻译成对该实参的 递归器 的调用。 由于每个类型正确的递归器使用都保证避免无限回归,这样的翻译即构成函数终止性的证据。 通过递归器定义的函数应用在定义上等同于递归结果,并且在内核中通常较为高效。
有些函数也难以改写为结构化递归;例如,某个函数之所以终止,是因为随着数组索引增大,索引与数组长度之差在减小,但此时由于增长的是函数的实参本身,Nat.rec
并不适用。
在这种情形下,存在一个随每次递归调用而减少的终止度量,但该度量本身并非函数的一个实参。
这时可以使用 良构递归 来定义函数。
良构递归是一种技术:系统地把“伴随度量递减的递归函数”转化为“基于证明的递归函数”,该证明表明任意度量递减序列最终会在最小值处终止。
用良构递归定义的函数应用不一定与其返回值在定义上相等,但这种相等可以作为命题来证明。
即便存在定义相等,这类函数在计算上仍常常较慢,因为它们需要归约通常很大的证明项。
一个函数的定义可以理解为一条给出其行为的方程。 在某些情况下,即使该递归函数对所有输入未必终止,仍可证明存在一个满足此规格的函数。 该策略甚至适用于某些函数定义对所有输入未必终止的情形。 由此得到的偏函数作为这些方程的不动点而出现,被称为 偏不动点。
尤其是,返回类型位于某些单子中的函数(例如 Option
)可以用该策略来定义。
对这类单子函数,Lean 还会生成额外的偏正确性定理。
与良构递归类似,按偏不动点定义的函数应用在定义上不等同于其返回值,但 Lean 会生成定理,在命题层面将该函数与其展开式以及定义中所给的归约行为相等同。
在许多应用中,某些函数的具体实现并不需要被推理。
一个递归函数可能仅作为证明自动化步骤实现的一部分,或仅是不会被形式化证明正确性的普通程序。
在这类场景中,Lean 内核不需要该定义在“定义相等”或“命题相等”层面成立;只要保持逻辑自洽即可。
被标记为 Lean.Parser.Command.declaration : command
partial
的函数会被内核视作不透明常量,既不会被展开也不会被归约。
为保持自洽性,唯一的要求是其返回类型可被占据(inhabited)。
偏函数在编译后的代码中仍可照常使用,也可出现在命题与证明中;只是它们在 Lean 逻辑中的等式理论非常薄弱。
不安全定义不受偏定义的任何限制。
它们可自由使用一般递归,并可使用会打破等式理论假设的 Lean 特性,例如强制转换原语(unsafeCast
)、检查指针相等(ptrAddrUnsafe
),以及观察引用计数(isExclusiveUnsafe
)。
但凡引用不安全定义的声明本身也必须标记为 Lean.Parser.Command.declaration : command
unsafe
,以清楚表明此处不保证逻辑自洽。
在编译后的代码中,不安全操作可用于以更高效的实现替换其他函数的实现,而内核仍然使用原始定义。
被替换的函数可以是不透明的,此时该函数名在逻辑中的等式理论是平凡的;也可以是普通函数,此时逻辑中仍会使用该函数。
请谨慎使用这一特性:逻辑自洽性虽不受威胁,但若不安全实现有误,Lean 程序的实际行为可能会偏离其经验证的逻辑模型。
如 繁释器输出概览 所述,递归函数的繁释分两阶段进行:
首先,按“Lean 的核心类型论自带递归定义”的假想来繁释该定义。除了使用递归之外,这个临时定义会被完全繁释。编译器会基于这些临时定义生成代码。
随后进行终止性分析,尝试用四种技术之一向 Lean 内核证明该函数是可接受的。若定义被标记为 Lean.Parser.Command.declaration : command
unsafe
或 Lean.Parser.Command.declaration : command
partial
,则直接采用相应技术。若给出了显式的 Lean.Parser.Command.declaration : command
termination_by
子句,则只尝试其中指明的技术。若无此类子句,繁释器会进行搜索:依次尝试将每个形参作为结构化递归的候选,并尝试寻找某个随每次递归调用而减少、且具良构关系的度量。
本节描述支配递归函数的规则。介绍互递归之后,将逐一给出这五类递归定义的规范,并讨论各自的推理能力与灵活性之间的权衡。
就像递归定义是在其定义体中提到正在被定义的名字一样,互递归 的定义指的是:它们本身可以是递归的,或彼此相互引用。 要在多个声明之间使用互递归,必须把它们放入一个 互递归块 中。
互递的一般语法为:
command ::= ... | mutual declaration* end
其中各声明必须是定义或定理。
在一个互递声明块中,各声明的名称不在彼此的类型签名的作用域内,但在彼此的定义体中可见。 尽管这些名称不在签名的作用域内,它们也不会被当作自动绑定的隐式参数插入。
递归定义的繁释总是在互递块这一粒度上进行;即便某个声明并不处在互递块中,也会好比其周围包了一层单元素的互递块。
通过 Lean.Parser.Term.letrec : term
let rec
与
Lean.Parser.Command.declaration : command
where
引入的局部定义会被从其上下文提升出去;必要时为捕获到的自由变量引入参数;并被视作 Lean.Parser.Command.mutual : command
mutual
块中的独立定义。
因此,写在 Lean.Parser.Command.declaration : command
where
块中的辅助定义,既可以彼此互递归,也可以和所在的主体定义互递归,但它们不能在彼此的类型签名中相互引用。
在繁释的第一步结束后(此时定义仍是递归的),在使用上述技术消解递归之前,Lean 会在互递块中的这些定义里识别出真正(互相)递归的团簇,并按照依赖顺序分别处理它们。
Structurally recursive functions are those in which each recursive call is on a structurally smaller term than the argument. The same parameter must decrease in all recursive calls; this parameter is called the decreasing parameter. Structural recursion is stronger than the primitive recursion that recursors provide, because the recursive call can use more deeply nested sub-terms of the argument, rather than only an immediate sub-term. The constructions used to implement structural recursion are, however, implemented using the recursor; these helper constructions are described in the section on inductive types.
The rules that govern structural recursion are fundamentally syntactic in nature. There are many recursive definitions that exhibit structurally recursive computational behavior, but which are not accepted by these rules; this is a fundamental consequence of the analysis being fully automatic. Well-founded recursion provides a semantic approach to demonstrating termination that can be used in situations where a recursive function is not structurally recursive, but it can also be used when a function that computes according to structural recursion doesn't satisfy the syntactic requirements.
The function countdown
is structurally recursive.
The parameter n
was matched against the pattern n' + 1
, which means that n'
is a direct subterm of n
in the second branch of the pattern match:
def countdown (n : Nat) : List Nat :=
match n with
| 0 => []
| n' + 1 => n' :: countdown n'
Replacing pattern matching with an equivalent Boolean test and subtraction results in an error:
def countdown' (n : Nat) : List Nat :=
if n == 0 then []
else
let n' := n - 1
n' :: countdown' n'
fail to show termination for countdown' with errors failed to infer structural recursion: Cannot use parameter n: failed to eliminate recursive application countdown' n' failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goal n : Nat h✝ : ¬(n == 0) = true n' : Nat := n - 1 ⊢ n - 1 < n
This is because there was no pattern matching on the parameter n
.
While this function indeed terminates, the argument that it does so is based on properties of if, the equality test, and subtraction, rather than being a generic feature of Nat
being an inductive type.
These arguments are expressed using well-founded recursion, and a slight change to the function definition allows Lean's automatic support for well-founded recursion to construct an alternative termination proof.
This version branches on the decidability of propositional equality for Nat
rather than the result of a Boolean equality test:
def countdown' (n : Nat) : List Nat :=
if n = 0 then []
else
let n' := n - 1
n' :: countdown' n'
Here, Lean's automation automatically constructs a termination proof from facts about propositional equality and subtraction. It uses well-founded recursion rather than structural recursion behind the scenes.
Structural recursion may be used explicitly or automatically. With explicit structural recursion, the function definition declares which parameter is the decreasing parameter. If no termination strategy is explicitly declared, Lean performs a search for a decreasing parameter as well as a decreasing measure for use with well-founded recursion. Explicitly annotating structural recursion has the following benefits:
It can speed up elaboration, because no search occurs.
It documents the termination argument for readers.
In situations where structural recursion is explicitly desired, it prevents the accidental use of well-founded recursion.
To explicitly use structural recursion, a function or theorem definition can be annotated with a Lean.Parser.Command.declaration : command
termination_by structural
clause that specifies the decreasing parameter.
The decreasing parameter may be a reference to a parameter named in the signature.
When the signature specifies a function type, the decreasing parameter may additionally be a parameter not named in the signature; in this case, names for the remaining parameters may be introduced by writing them before an arrow (Lean.Parser.Command.declaration : command
=>
).
When the decreasing parameter is a named parameter to the function, it can be specified by referring to its name.
def half (n : Nat) : Nat :=
match n with
| 0 | 1 => 0
| n + 2 => half n + 1
termination_by structural n
When the decreasing parameter is not named in the signature, a name can be introduced locally in the Lean.Parser.Command.declaration : command
termination_by
clause.
def half : Nat → Nat
| 0 | 1 => 0
| n + 2 => half n + 1
termination_by structural n => n
The termination_by structural
clause introduces a decreasing parameter.
terminationBy ::= ... |
Specify a termination measure for recursive functions. ``` termination_by a - b ``` indicates that termination of the currently defined recursive function follows because the difference between the arguments `a` and `b` decreases. If the function takes further argument after the colon, you can name them as follows: ``` def example (a : Nat) : Nat → Nat → Nat := termination_by b c => a - b ``` By default, a `termination_by` clause will cause the function to be constructed using well-founded recursion. The syntax `termination_by structural a` (or `termination_by structural _ c => c`) indicates the function is expected to be structural recursive on the argument. In this case the body of the `termination_by` clause must be one of the function's parameters. If omitted, a termination measure will be inferred. If written as `termination_by?`, the inferrred termination measure will be suggested.
termination_by structural (ident* =>)? term
Specify a termination measure for recursive functions. ``` termination_by a - b ``` indicates that termination of the currently defined recursive function follows because the difference between the arguments `a` and `b` decreases. If the function takes further argument after the colon, you can name them as follows: ``` def example (a : Nat) : Nat → Nat → Nat := termination_by b c => a - b ``` By default, a `termination_by` clause will cause the function to be constructed using well-founded recursion. The syntax `termination_by structural a` (or `termination_by structural _ c => c`) indicates the function is expected to be structural recursive on the argument. In this case the body of the `termination_by` clause must be one of the function's parameters. If omitted, a termination measure will be inferred. If written as `termination_by?`, the inferrred termination measure will be suggested.
The identifiers before the optional =>
can bring function parameters into scope that are not
already bound in the declaration header, and the mandatory term must indicate one of the function's parameters, whether introduced in the header or locally in the clause.
The decreasing parameter must satisfy the following conditions:
Its type must be an inductive type.
If its type is an indexed family, then all indices must be parameters of the function.
If the inductive or indexed family of the decreasing parameter has data type parameters, then these data type parameters may themselves only depend on function parameters that are part of the fixed prefix.
A fixed parameter is a function parameter that is passed unmodified in all recursive calls and is not an index of the recursive parameter's type. The fixed prefix is the longest prefix of the function's parameters in which all are fixed.
The decreasing parameter's type must be an inductive type.
In notInductive
, a function is specified as the decreasing parameter:
def notInductive (x : Nat → Nat) : Nat := notInductive (fun n => x (n+1))
termination_by structural x
cannot use specified measure for structural recursion: its type is not an inductive
If the decreasing parameter is an indexed family, all the indices must be variables.
In constantIndex
, the indexed family Fin'
is instead applied to a constant value:
inductive Fin' : Nat → Type where
| zero : Fin' (n+1)
| succ : Fin' n → Fin' (n+1)
def constantIndex (x : Fin' 100) : Nat := constantIndex .zero
termination_by structural x
cannot use specified measure for structural recursion: its type Fin' is an inductive family and indices are not variables Fin' 100
The parameters of the decreasing parameter's type must not depend on function parameters that come after varying parameters or indices.
In afterVarying
, the fixed prefix is empty, because the first parameter n
varies, so p
is not part of the fixed prefix:
inductive WithParam' (p : Nat) : Nat → Type where
| zero : WithParam' p (n+1)
| succ : WithParam' p n → WithParam' p (n+1)
def afterVarying (n : Nat) (p : Nat) (x : WithParam' p n) : Nat :=
afterVarying (n+1) p .zero
termination_by structural x
failed to infer structural recursion: Cannot use parameter x: failed to eliminate recursive application afterVarying (n + 1) p WithParam'.zero
Furthermore, every recursive call of the functions must be on a strict sub-term of the decreasing parameter.
The decreasing parameter itself is a sub-term, but not a strict sub-term.
If a sub-term is the discriminant of a Lean.Parser.Term.match : term
Pattern matching. `match e, ... with | p, ... => f | ...` matches each given
term `e` against each pattern `p` of a match alternative. When all patterns
of an alternative match, the `match` term evaluates to the value of the
corresponding right-hand side `f` with the pattern variables bound to the
respective matched values.
If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available
within `f`.
When not constructing a proof, `match` does not automatically substitute variables
matched on in dependent variables' types. Use `match (generalizing := true) ...` to
enforce this.
Syntax quotations can also be used in a pattern match.
This matches a `Syntax` value against quotations, pattern variables, or `_`.
Quoted identifiers only match identical identifiers - custom matching such as by the preresolved
names only should be done explicitly.
`Syntax.atom`s are ignored during matching by default except when part of a built-in literal.
For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they
should participate in matching.
For example, in
```lean
syntax "c" ("foo" <|> "bar") ...
```
`foo` and `bar` are indistinguishable during matching, but in
```lean
syntax foo := "foo"
syntax "c" (foo <|> "bar") ...
```
they are not.
match
expression or other pattern-matching syntax, the pattern that matches the discriminant is a sub-term in the right-hand side of each match alternative.
In particular, the rules of match generalization are used to connect the discriminant to the occurrences of the pattern term in the right-hand side; thus, it respects definitional equality.
The pattern is a strict sub-term if and only if the discriminant is a strict sub-term.
If a sub-term is a constructor applied to arguments, then its recursive arguments are strict sub-terms.
In the following example, the decreasing parameter n
is matched against the nested pattern .succ (.succ n)
. Therefore .succ (.succ n)
is a (non-strict) sub-term of n
, and consequently both n
and .succ n
are strict sub-terms, and the definition is accepted.
def fib : Nat → Nat
| 0 | 1 => 1
| .succ (.succ n) => fib n + fib (.succ n)
termination_by structural n => n
For clarity, this example uses .succ n
and .succ (.succ n)
instead of the equivalent Nat
-specific n+1
and n+2
.
In the following example, the decreasing parameter n
is not directly the discriminant of the Lean.Parser.Term.match : term
Pattern matching. `match e, ... with | p, ... => f | ...` matches each given
term `e` against each pattern `p` of a match alternative. When all patterns
of an alternative match, the `match` term evaluates to the value of the
corresponding right-hand side `f` with the pattern variables bound to the
respective matched values.
If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available
within `f`.
When not constructing a proof, `match` does not automatically substitute variables
matched on in dependent variables' types. Use `match (generalizing := true) ...` to
enforce this.
Syntax quotations can also be used in a pattern match.
This matches a `Syntax` value against quotations, pattern variables, or `_`.
Quoted identifiers only match identical identifiers - custom matching such as by the preresolved
names only should be done explicitly.
`Syntax.atom`s are ignored during matching by default except when part of a built-in literal.
For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they
should participate in matching.
For example, in
```lean
syntax "c" ("foo" <|> "bar") ...
```
`foo` and `bar` are indistinguishable during matching, but in
```lean
syntax foo := "foo"
syntax "c" (foo <|> "bar") ...
```
they are not.
match
expression.
Therefore, n'
is not considered a sub-term of n
.
def half (n : Nat) : Nat :=
match Option.some n with
| .some (n' + 2) => half n' + 1
| _ => 0
termination_by structural n
failed to infer structural recursion: Cannot use parameter n: failed to eliminate recursive application half n'
Using well-founded recursion, and explicitly connecting the discriminant to the pattern of the match, this definition can be accepted.
def half (n : Nat) : Nat :=
match h : Option.some n with
| .some (n' + 2) => half n' + 1
| _ => 0
termination_by n
decreasing_by n:Natn':Nath:n = n' + 1 + 1⊢ n' < n' + 1 + 1; All goals completed! 🐙
Similarly, the following example fails: although xs.tail
would reduce to a strict sub-term of xs
, this is not visible to Lean according to the rules above.
In particular, xs.tail
is not definitionally equal to a strict sub-term of xs
.
def listLen : List α → Nat
| [] => 0
| xs => listLen xs.tail + 1
termination_by structural xs => xs
An important consequence of the strategies that are used to prove termination is that simultaneous matching of two discriminants is not equivalent to matching a pair.
Simultaneous matching maintains the connection between the discriminants and the patterns, allowing the pattern matching to refine the types of the assumptions in the local context as well as the expected type of the Lean.Parser.Term.match : term
Pattern matching. `match e, ... with | p, ... => f | ...` matches each given
term `e` against each pattern `p` of a match alternative. When all patterns
of an alternative match, the `match` term evaluates to the value of the
corresponding right-hand side `f` with the pattern variables bound to the
respective matched values.
If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available
within `f`.
When not constructing a proof, `match` does not automatically substitute variables
matched on in dependent variables' types. Use `match (generalizing := true) ...` to
enforce this.
Syntax quotations can also be used in a pattern match.
This matches a `Syntax` value against quotations, pattern variables, or `_`.
Quoted identifiers only match identical identifiers - custom matching such as by the preresolved
names only should be done explicitly.
`Syntax.atom`s are ignored during matching by default except when part of a built-in literal.
For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they
should participate in matching.
For example, in
```lean
syntax "c" ("foo" <|> "bar") ...
```
`foo` and `bar` are indistinguishable during matching, but in
```lean
syntax foo := "foo"
syntax "c" (foo <|> "bar") ...
```
they are not.
match
.
Essentially, the elaboration rules for Lean.Parser.Term.match : term
Pattern matching. `match e, ... with | p, ... => f | ...` matches each given
term `e` against each pattern `p` of a match alternative. When all patterns
of an alternative match, the `match` term evaluates to the value of the
corresponding right-hand side `f` with the pattern variables bound to the
respective matched values.
If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available
within `f`.
When not constructing a proof, `match` does not automatically substitute variables
matched on in dependent variables' types. Use `match (generalizing := true) ...` to
enforce this.
Syntax quotations can also be used in a pattern match.
This matches a `Syntax` value against quotations, pattern variables, or `_`.
Quoted identifiers only match identical identifiers - custom matching such as by the preresolved
names only should be done explicitly.
`Syntax.atom`s are ignored during matching by default except when part of a built-in literal.
For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they
should participate in matching.
For example, in
```lean
syntax "c" ("foo" <|> "bar") ...
```
`foo` and `bar` are indistinguishable during matching, but in
```lean
syntax foo := "foo"
syntax "c" (foo <|> "bar") ...
```
they are not.
match
treat the discriminants specially, and changing discriminants in a way that preserves the run-time meaning of a program does not necessarily preserve the compile-time meaning.
This function that finds the minimum of two natural numbers is defined by structural recursion on its first parameter:
def min' (n k : Nat) : Nat :=
match n, k with
| 0, _ => 0
| _, 0 => 0
| n' + 1, k' + 1 => min' n' k' + 1
termination_by structural n
Replacing the simultaneous pattern match on both parameters with a match on a pair causes termination analysis to fail:
def min' (n k : Nat) : Nat :=
match (n, k) with
| (0, _) => 0
| (_, 0) => 0
| (n' + 1, k' + 1) => min' n' k' + 1
termination_by structural n
failed to infer structural recursion: Cannot use parameter n: failed to eliminate recursive application min' n' k'
This is because the analysis only considers direct pattern matching on parameters when matching recursive calls to strictly-smaller argument values. Wrapping the discriminants in a pair breaks the connection.
This function that finds the minimum of the two components of a pair can't be elaborated via structural recursion.
def min' (nk : Nat × Nat) : Nat :=
match nk with
| (0, _) => 0
| (_, 0) => 0
| (n' + 1, k' + 1) => min' (n', k') + 1
termination_by structural nk
failed to infer structural recursion: Cannot use parameter nk: the type Nat × Nat does not have a `.brecOn` recursor
This is because the parameter's type, Prod
, is not recursive.
Thus, its constructor has no recursive parameters that can be exposed by pattern matching.
This definition is accepted using well-founded recursion, however:
def min' (nk : Nat × Nat) : Nat :=
match nk with
| (0, _) => 0
| (_, 0) => 0
| (n' + 1, k' + 1) => min' (n', k') + 1
termination_by nk
Even though the recursive occurrence of countdown
is applied to a term that is not a strict sub-term of the decreasing parameter, the following definition is accepted:
def countdown (n : Nat) : List Nat :=
match n with
| 0 => []
| n' + 1 => n' :: countdown (n' + 0)
termination_by structural n
This is because n' + 0
is definitionally equal to n'
, which is a strict sub-term of n
.
Sub-terms that result from pattern matching are connected to the discriminant using the rules for match generalization, which respect definitional equality.
In countdown'
, the recursive occurrence is applied to 0 + n'
, which is not definitionally equal to n'
because addition on natural numbers is structurally recursive in its second parameter:
def countdown' (n : Nat) : List Nat :=
match n with
| 0 => []
| n' + 1 => n' :: countdown' (0 + n')
termination_by structural n
failed to infer structural recursion: Cannot use parameter n: failed to eliminate recursive application countdown' (0 + n')
Lean supports the definition of mutually recursive functions using structural recursion.
Mutual recursion may be introduced using a mutual block, but it also results from Lean.Parser.Term.letrec : term
let rec
expressions and Lean.Parser.Command.declaration : command
where
blocks.
The rules for mutual structural recursion are applied to a group of actually mutually recursive, lifted definitions, that results from the elaboration steps for mutual groups.
If every function in the mutual group has a termination_by structural
annotation indicating that function’s decreasing argument, then structural recursion is used to translate the definitions.
The requirements on the decreasing argument above are extended:
All the types of all the decreasing arguments must be from the same inductive type, or more generally from the same mutual group of inductive types.
The parameters of the decreasing parameter's types must be the same for all functions, and may depend only on the common fixed prefix of function arguments.
The functions do not have to be in a one-to-one correspondence to the mutual inductive types. Multiple functions can have a decreasing argument of the same type, and not all types that are mutually recursive with the decreasing argument need have a corresponding function.
The following example demonstrates recursion over mutually inductive types.
The functions Exp.size
and App.size
are mutually recursive.
mutual
inductive Exp where
| var : String → Exp
| app : App → Exp
inductive App where
| fn : String → App
| app : App → Exp → App
end
mutual
def Exp.size : Exp → Nat
| .var _ => 1
| .app a => a.size
termination_by structural e => e
def App.size : App → Nat
| .fn _ => 1
| .app a e => a.size + e.size + 1
termination_by structural a => a
end
The definition of App.numArgs
is structurally recursive over type App
.
It demonstrates that not all inductive types in the mutual group need to be handled.
def App.numArgs : App → Nat
| .fn _ => 0
| .app a _ => a.numArgs + 1
termination_by structural a => a
If no termination_by
clauses are present in a recursive or mutually recursive function definition, then Lean attempts to infer a suitable structurally decreasing argument, effectively by trying all suitable parameters in sequence.
If this search fails, Lean then attempts to infer well-founded recursion.
For mutually recursive functions, all combinations of parameters are tried, up to a limit to avoid combinatorial explosion.
If only some of the mutually recursive functions have termination_by structural
clauses, then only those parameters are considered, while for the other functions all parameters are considered for structural recursion.
A termination_by?
clause causes the inferred termination annotation to be shown.
It can be automatically added to the source file using the offered suggestion or code action.
Lean automatically infers that the function half
is structurally recursive.
The termination_by?
clause causes the inferred termination annotation to be displayed, and it can be automatically added to the source file with a single click.
def half : Nat → Nat
| 0 | 1 => 0
| n + 2 => half n + 1
termination_by?
Try this: termination_by structural x => x
In this section, the construction used to elaborate structurally recursive functions is explained in more detail.
This elaboration uses the below
and brecOn
constructions that are automatically generated from inductive types' recursors.
Addition of natural numbers can be defined via recursion on the second argument. This function is straightforwardly structurally recursive.
def add (n : Nat) : Nat → Nat
| .zero => n
| .succ k => .succ (add n k)
Defined using Nat.rec
, it is much further from the notations that most people are used to.
def add' (n : Nat) :=
Nat.rec (motive := fun _ => Nat)
n
(fun k soFar => .succ soFar)
Structural recursive calls made on data that isn't the immediate child of the function parameter requires either creativity or a complex yet systematic encoding.
def half : Nat → Nat
| 0 | 1 => 0
| n + 2 => half n + 1
One way to think about this function is as a structural recursion that flips a bit at each call, only incrementing the result when the bit is set.
def helper : Nat → Bool → Nat :=
Nat.rec (motive := fun _ => Bool → Nat)
(fun _ => 0)
(fun _ soFar =>
fun b =>
(if b then Nat.succ else id) (soFar !b))
def half' (n : Nat) : Nat := helper n false
#eval [0, 1, 2, 3, 4, 5, 6, 7, 8].map half'
[0, 0, 1, 1, 2, 2, 3, 3, 4]
Instead of creativity, a general technique called course-of-values recursion can be used.
Course-of-values recursion uses helpers that can be systematically derived for every inductive type, defined in terms of the recursor; Lean derives them automatically.
For every Nat
n
, the type n.below (motive := mot)
provides a value of type mot k
for all k < n
, represented as an iterated dependent pair type.
The course-of-values recursor Nat.brecOn
allows a function to use the result for any smaller Nat
.
Using it to define the function is inconvenient:
noncomputable def half'' (n : Nat) : Nat :=
Nat.brecOn n (motive := fun _ => Nat)
fun k soFar =>
match k, soFar with
| 0, _ | 1, _ => 0
| _ + 2, ⟨_, ⟨h, _⟩⟩ => h + 1
The function is marked Lean.Parser.Command.declaration : command
noncomputable
because the compiler doesn't support generating code for course-of-values recursion, which is intended for reasoning rather that efficient code.
The kernel can still be used to test the function, however:
#reduce [0,1,2,3,4,5,6,7,8].map half''
[0, 0, 1, 1, 2, 2, 3, 3, 4]
The dependent pattern matching in the body of half''
can also be encoded using recursors (specifically, Nat.casesOn
), if necessary:
noncomputable def half''' (n : Nat) : Nat :=
n.brecOn (motive := fun _ => Nat)
fun k =>
k.casesOn
(motive :=
fun k' =>
(k'.below (motive := fun _ => Nat)) →
Nat)
(fun _ => 0)
(fun k' =>
k'.casesOn
(motive :=
fun k'' =>
(k''.succ.below (motive := fun _ => Nat)) →
Nat)
(fun _ => 0)
(fun _ soFar => soFar.2.1.succ))
This definition still works.
#reduce [0,1,2,3,4,5,6,7,8].map half''
[0, 0, 1, 1, 2, 2, 3, 3, 4]
However, it is now far from the original definition and it has become difficult for most people to understand. Recursors are an excellent logical foundation, but not an easy way to write programs or proofs.
The structural recursion analysis attempts to translate the recursive pre-definition into a use of the appropriate structural recursion constructions.
At this step, pattern matching has already been translated into the use of matcher functions; these are treated specially by the termination checker.
Next, for each group of parameters, a translation using brecOn
is attempted.
This definition is equivalent to List.below
:
def List.below' {α : Type u} {motive : List α → Sort u} :
List α → Sort (max 1 u)
| [] => PUnit
| _ :: xs => motive xs ×' xs.below' (motive := motive)
In other words, for a given motive, List.below'
is a type that contains a realization of the motive for all suffixes of the list.
More recursive arguments require further nested iterations of the product type. For instance, binary trees have two recursive occurrences.
inductive Tree (α : Type u) : Type u where
| leaf
| branch (left : Tree α) (val : α) (right : Tree α)
Its corresponding course-of-values table contains the realizations of the motive for all subtrees:
def Tree.below' {α : Type u} {motive : Tree α → Sort u} :
Tree α → Sort (max 1 u)
| .leaf => PUnit
| .branch left _val right =>
(motive left ×' left.below' (motive := motive)) ×'
(motive right ×' right.below' (motive := motive))
For both lists and trees, the brecOn
operator expects just a single case, rather than one per constructor.
This case accepts a list or tree along with a table of results for all smaller values; from this, it should satisfy the motive for the provided value.
Dependent case analysis of the provided value automatically refines the type of the memo table, providing everything needed.
The following definitions are equivalent to List.brecOn
and Tree.brecOn
, respectively.
The primitive recursive helpers List.brecOnTable
and Tree.brecOnTable
compute the course-of-values tables along with the final results, and the actual definitions of the brecOn
operators simply project out the result.
def List.brecOnTable {α : Type u}
{motive : List α → Sort u}
(xs : List α)
(step :
(ys : List α) →
ys.below' (motive := motive) →
motive ys) :
motive xs ×' xs.below' (motive := motive) :=
match xs with
| [] => ⟨step [] PUnit.unit, PUnit.unit⟩
| x :: xs =>
let res := xs.brecOnTable (motive := motive) step
let val := step (x :: xs) res
⟨val, res⟩
def Tree.brecOnTable {α : Type u}
{motive : Tree α → Sort u}
(t : Tree α)
(step :
(ys : Tree α) →
ys.below' (motive := motive) →
motive ys) :
motive t ×' t.below' (motive := motive) :=
match t with
| .leaf => ⟨step .leaf PUnit.unit, PUnit.unit⟩
| .branch left val right =>
let resLeft := left.brecOnTable (motive := motive) step
let resRight := right.brecOnTable (motive := motive) step
let branchRes := ⟨resLeft, resRight⟩
let val := step (.branch left val right) branchRes
⟨val, branchRes⟩
def List.brecOn' {α : Type u}
{motive : List α → Sort u}
(xs : List α)
(step :
(ys : List α) →
ys.below' (motive := motive) →
motive ys) :
motive xs :=
(xs.brecOnTable (motive := motive) step).1
def Tree.brecOn' {α : Type u}
{motive : Tree α → Sort u}
(t : Tree α)
(step :
(ys : Tree α) →
ys.below' (motive := motive) →
motive ys) :
motive t :=
(t.brecOnTable (motive := motive) step).1
The below
construction is a mapping from each value of a type to the results of some function call on all smaller values; it can be understood as a memoization table that already contains the results for all smaller values.
The notion of “smaller value” that is expressed in the below
construction corresponds directly to the definition of strict sub-terms.
Recursors expect an argument for each of the inductive type's constructors; these arguments are called with the constructor's arguments (and the result of recursion on recursive parameters) during ι-reduction.
The course-of-values recursion operator brecOn
, on the other hand, expects just a single case that covers all constructors at once.
This case is provided with a value and a below
table that contains the results of recursion on all values smaller than the given value; it should use the contents of the table to satisfy the motive for the provided value.
If the function is structurally recursive over a given parameter (or parameter group), then the results of all recursive calls will be present in this table already.
When the body of the recursive function is transformed into an invocation of brecOn
on one of the function's parameters, the parameter and its course-of-values table are in scope.
The analysis traverses the body of the function, looking for recursive calls.
If the parameter is matched against, then its occurrences in the local context are generalized and then instantiated with the pattern; this is also true for the type of the course-of-values table.
Typically, this pattern matching results in the type of the course-of-values table becoming more specific, which gives access to the recursive results for smaller values.
This generalization process implements the rule that patterns are sub-terms of match discriminants.
When an recursive occurrence of the function is detected, the course-of-values table is consulted to see whether it contains a result for the argument being checked.
If so, the recursive call can be replaced with a projection from the table.
If not, then the parameter in question doesn't support structural recursion.
The first step in walking through the elaboration of half
is to manually desugar it to a simpler form.
This doesn't match the way Lean works, but its output is much easier to read when there are fewer OfNat
instances present.
This readable definition:
def half : Nat → Nat
| 0 | 1 => 0
| n + 2 => half n + 1
can be rewritten to this somewhat lower-level version:
def half : Nat → Nat
| .zero | .succ .zero => .zero
| .succ (.succ n) => half n |>.succ
The elaborator begins by elaborating a pre-definition in which recursion is still present but the definition is otherwise in Lean's core type theory. Turning on the compiler's tracing of pre-definitions, as well as making the pretty printer more explicit, makes the resulting pre-definition visible:
set_option trace.Elab.definition.body true in
set_option pp.all true in
def half : Nat → Nat
| .zero | .succ .zero => .zero
| .succ (.succ n) => half n |>.succ
The returned trace message is:
[Elab.definition.body] half : Nat → Nat := fun (x : Nat) => half.match_1.{1} (fun (x : Nat) => Nat) x (fun (_ : Unit) => Nat.zero) (fun (_ : Unit) => Nat.zero) fun (n : Nat) => Nat.succ (half n)
The auxiliary match function's definition is:
#print half.match_1
def half.match_1.{u_1} : (motive : Nat → Sort u_1) → (x : Nat) → (Unit → motive Nat.zero) → (Unit → motive 1) → ((n : Nat) → motive n.succ.succ) → motive x := fun motive x h_1 h_2 h_3 => Nat.casesOn x (h_1 ()) fun n => Nat.casesOn n (h_2 ()) fun n => h_3 n
Formatted more readably, this definition is:
def half.match_1'.{u} :
(motive : Nat → Sort u) → (x : Nat) →
(Unit → motive Nat.zero) → (Unit → motive 1) →
((n : Nat) → motive n.succ.succ) →
motive x :=
fun motive x h_1 h_2 h_3 =>
Nat.casesOn x (h_1 ()) fun n =>
Nat.casesOn n (h_2 ()) fun n =>
h_3 n
In other words, the specific configuration of patterns used in half
are captured in half.match_1
.
This definition is a more readable version of half
's pre-definition:
def half' : Nat → Nat :=
fun (x : Nat) =>
half.match_1 (motive := fun _ => Nat) x
(fun _ => 0) -- Case for 0
(fun _ => 0) -- Case for 1
(fun n => Nat.succ (half' n)) -- Case for n + 2
To elaborate it as a structurally recursive function, the first step is to establish the bRecOn
invocation.
The definition must be marked Lean.Parser.Command.declaration : command
noncomputable
because Lean does not support code generation for recursors such as Nat.brecOn
.
noncomputable
def half'' : Nat → Nat :=
fun (x : Nat) =>
x.brecOn fun n table =>
_
/- To translate:
half.match_1 (motive := fun _ => Nat) x
(fun _ => 0) -- Case for 0
(fun _ => 0) -- Case for 1
(fun n => Nat.succ (half' n)) -- Case for n + 2
-/
The next step is to replace occurrences of x
in the original function body with the n
provided by brecOn
.
Because table
's type depends on x
, it must also be generalized when splitting cases with half.match_1
, leading to a motive with an extra parameter.
noncomputable
def half'' : Nat → Nat :=
fun (x : Nat) =>
x.brecOn fun n table =>
(half.match_1
(motive :=
fun k =>
k.below (motive := fun _ => Nat) →
Nat)
n
_
_
_)
table
/- To translate:
(fun _ => 0) -- Case for 0
(fun _ => 0) -- Case for 1
(fun n => Nat.succ (half' n)) -- Case for n + 2
-/
The three cases' placeholders expect the following types:
don't know how to synthesize placeholder for argument 'h_1' context: x n : Nat table : Nat.below n ⊢ Unit → (fun k => Nat.below k → Nat) Nat.zero
don't know how to synthesize placeholder for argument 'h_2' context: x n : Nat table : Nat.below n ⊢ Unit → (fun k => Nat.below k → Nat) 1
don't know how to synthesize placeholder for argument 'h_3' context: x n : Nat table : Nat.below n ⊢ (n : Nat) → (fun k => Nat.below k → Nat) n.succ.succ
The first two cases in the pre-definition are constant functions, with no recursion to check:
noncomputable
def half'' : Nat → Nat :=
fun (x : Nat) =>
x.brecOn fun n table =>
(half.match_1
(motive :=
fun k =>
k.below (motive := fun _ => Nat) →
Nat)
n
(fun () _ => .zero)
(fun () _ => .zero)
_)
table
/- To translate:
(fun n => Nat.succ (half' n)) -- Case for n + 2
-/
The final case contains a recursive call. It should be translated into a lookup into the course-of-values table. A more readable representation of the last hole's type is:
(n : Nat) →
Nat.below (motive := fun _ => Nat) n.succ.succ →
Nat
which is equivalent to
(n : Nat) →
Nat ×' (Nat ×' Nat.below (motive := fun _ => Nat) n) →
Nat
The first Nat
in the course-of-values table is the result of recursion on n + 1
, and the second is the result of recursion on n
.
The recursive call can thus be replaced by a lookup, and the elaboration is successful:
noncomputable
def half'' : Nat → Nat :=
fun (x : Nat) =>
x.brecOn fun n table =>
(half.match_1
(motive :=
fun k =>
k.below (motive := fun _ => Nat) →
Nat)
n
(fun () _ => .zero)
(fun () _ => .zero)
(fun _ table => Nat.succ table.2.1)
table
The actual elaborator keeps track of the relationship between the parameter being checked for structural recursion and the positions in the course-of-values tables by inserting sentinel types with fresh names into the motive.
Functions defined by well-founded recursion are those in which each recursive call has arguments that are smaller (in a suitable sense) than the functions' parameters. In contrast to structural recursion, in which recursive definitions must satisfy particular syntactic requirements, definitions that use well-founded recursion employ semantic arguments. This allows a larger class of recursive definitions to be accepted. Furthermore, when Lean's automation fails to construct a termination proof, it is possible to specify one manually.
All definitions are treated identically by the Lean compiler. In Lean's logic, definitions that use well-founded recursion typically do not reduce definitionally. The reductions do hold as propositional equalities, however, and Lean automatically proves them. This does not typically make it more difficult to prove properties of definitions that use well-founded recursion, because the propositional reductions can be used to reason about the behavior of the function. It does mean, however, that using these functions in types typically does not work well. Even when the reduction behavior happens to hold definitionally, it is often much slower than structurally recursive definitions in the kernel, which must unfold the termination proof along with the definition. When possible, recursive function that are intended for use in types or in other situations where definitional equality is important should be defined with structural recursion.
To explicitly use well-founded recursion, a function or theorem definition can be annotated with a Lean.Parser.Command.declaration : command
termination_by
clause that specifies the measure by which the function terminates.
The measure should be a term that decreases at each recursive call; it may be one of the function's parameters or a tuple of the parameters, but it may also be any other term.
The measure's type must be equipped with a well-founded relation, which determines what it means for the measure to decrease.
The Lean.Parser.Command.declaration : command
termination_by
clause introduces the termination argument.
terminationBy ::= ... |
Specify a termination measure for recursive functions. ``` termination_by a - b ``` indicates that termination of the currently defined recursive function follows because the difference between the arguments `a` and `b` decreases. If the function takes further argument after the colon, you can name them as follows: ``` def example (a : Nat) : Nat → Nat → Nat := termination_by b c => a - b ``` By default, a `termination_by` clause will cause the function to be constructed using well-founded recursion. The syntax `termination_by structural a` (or `termination_by structural _ c => c`) indicates the function is expected to be structural recursive on the argument. In this case the body of the `termination_by` clause must be one of the function's parameters. If omitted, a termination measure will be inferred. If written as `termination_by?`, the inferrred termination measure will be suggested.
termination_by (ident* =>)? term
Specify a termination measure for recursive functions. ``` termination_by a - b ``` indicates that termination of the currently defined recursive function follows because the difference between the arguments `a` and `b` decreases. If the function takes further argument after the colon, you can name them as follows: ``` def example (a : Nat) : Nat → Nat → Nat := termination_by b c => a - b ``` By default, a `termination_by` clause will cause the function to be constructed using well-founded recursion. The syntax `termination_by structural a` (or `termination_by structural _ c => c`) indicates the function is expected to be structural recursive on the argument. In this case the body of the `termination_by` clause must be one of the function's parameters. If omitted, a termination measure will be inferred. If written as `termination_by?`, the inferrred termination measure will be suggested.
The identifiers before the optional =>
can bring function parameters into scope that are not
already bound in the declaration header, and the mandatory term must indicate one of the function's parameters, whether introduced in the header or locally in the clause.
Division can be specified as the number of times the divisor can be subtracted from the dividend.
This operation cannot be elaborated using structural recursion because subtraction is not pattern matching.
The value of n
does decrease with each recursive call, so well-founded recursion can be used to justify the definition of division by iterated subtraction.
def div (n k : Nat) : Nat :=
if k = 0 then 0
else if k > n then 0
else 1 + div (n - k) k
termination_by n
A relation ≺
is a well-founded relation if there exists no infinitely descending chain
x_0 ≻ x_1 ≻ \cdots
In Lean, types that are equipped with a canonical well-founded relation are instances of the WellFoundedRelation
type class.
WellFoundedRelation.{u} (α : Sort u) : Sort (max 1 u)WellFoundedRelation.{u} (α : Sort u) : Sort (max 1 u)
A type that has a standard well-founded relation.
Instances are used to prove that functions terminate using well-founded recursion by showing that recursive calls reduce some measure according to a well-founded relation. This relation can combine well-founded relations on the recursive function's parameters.
WellFoundedRelation.mk.{u}
rel : α → α → Prop
A well-founded relation on α
.
wf : WellFounded WellFoundedRelation.rel
A proof that rel
is, in fact, well-founded.
The most important instances are:
Nat
, ordered by (· < ·)
.
Prod
, ordered lexicographically: (a₁, b₁) ≺ (a₂, b₂)
if and only if a₁ ≺ a₂
or a₁ = a₂
and b₁ ≺ b₂
.
Every type that is an instance of the SizeOf
type class, which provides a method SizeOf.sizeOf
, has a well-founded relation.
For these types, x₁ ≺ x₂
if and only if sizeOf x₁ < sizeOf x₂
. For inductive types, a SizeOf
instance is automatically derived by Lean.
Note that there exists a low-priority instance instSizeOfDefault
that provides a SizeOf
instance for any type, and always returns 0
.
This instance cannot be used to prove that a function terminates using well-founded recursion because 0 < 0
is false.
Function types in general do not have a well-founded relation that's useful for termination proofs.
Instance synthesis thus selects instSizeOfDefault
and the corresponding well-founded relation.
If the measure is a function, the default SizeOf
instance is selected and the proof cannot succeed.
def fooInst (b : Bool → Bool) : Unit := fooInst (b ∘ b)
termination_by b
decreasing_by
b:Bool → Bool⊢ sizeOf (b ∘ b) < sizeOf b
b:Bool → Bool⊢ 0 < 0
b:Bool → Bool⊢ 0 < 0
b:Bool → Bool⊢ False
b:Bool → Bool⊢ False
All goals completed! 🐙
Once a measure is specified and its well-founded relation is determined, Lean determines the termination proof obligation for every recursive call.
The proof obligation for each recursive call is of the form g a₁ a₂ … ≺ g p₁ p₂ …
, where:
g
is the measure as a function of the parameters,
≺
is the inferred well-founded relation,
a₁
a₂
…
are the arguments of the recursive call and
p₁
p₂
…
are the parameters of the function definition.
The context of the proof obligation is the local context of the recursive call.
In particular, local assumptions (such as those introduced by if h : _
, match h : _ with
or have
) are available.
If a function parameter is the discriminant of a pattern match (e.g. by a Lean.Parser.Term.match : term
Pattern matching. `match e, ... with | p, ... => f | ...` matches each given
term `e` against each pattern `p` of a match alternative. When all patterns
of an alternative match, the `match` term evaluates to the value of the
corresponding right-hand side `f` with the pattern variables bound to the
respective matched values.
If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available
within `f`.
When not constructing a proof, `match` does not automatically substitute variables
matched on in dependent variables' types. Use `match (generalizing := true) ...` to
enforce this.
Syntax quotations can also be used in a pattern match.
This matches a `Syntax` value against quotations, pattern variables, or `_`.
Quoted identifiers only match identical identifiers - custom matching such as by the preresolved
names only should be done explicitly.
`Syntax.atom`s are ignored during matching by default except when part of a built-in literal.
For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they
should participate in matching.
For example, in
```lean
syntax "c" ("foo" <|> "bar") ...
```
`foo` and `bar` are indistinguishable during matching, but in
```lean
syntax foo := "foo"
syntax "c" (foo <|> "bar") ...
```
they are not.
match
expression), then this parameter is refined to the matched pattern in the proof obligation.
The overall termination proof obligation consists of one goal for each recursive call.
By default, the tactic decreasing_trivial
is used to prove each proof obligation.
A custom tactic script can be provided using the optional Lean.Parser.Command.declaration : command
decreasing_by
clause, which comes after the Lean.Parser.Command.declaration : command
termination_by
clause.
This tactic script is run once, with one goal for each proof obligation, rather than separately on each proof obligation.
The following recursive definition of the Fibonacci numbers has two recursive calls, which results in two goals in the termination proof.
def fib (n : Nat) :=
if h : n ≤ 1 then
1
else
fib (n - 1) + fib (n - 2)
termination_by n
decreasing_by
n:Nath:¬n ≤ 1⊢ n - 1 < nn:Nath:¬n ≤ 1⊢ n - 2 < n
Here, the measure is simply the parameter itself, and the well-founded order is the less-than relation on natural numbers.
The first proof goal requires the user to prove that the argument of the first recursive call, namely n - 1
, is strictly smaller than the function's parameter, n
.
Both termination proofs can be easily discharged using the omega
tactic.
def fib (n : Nat) :=
if h : n ≤ 1 then
1
else
fib (n - 1) + fib (n - 2)
termination_by n
decreasing_by
n:Nath:¬n ≤ 1⊢ n - 1 < n All goals completed! 🐙
n:Nath:¬n ≤ 1⊢ n - 2 < n All goals completed! 🐙
Additionally, the context is enriched with additional assumptions that can make it easier to prove termination. Some examples include:
In the branches of an if-then-else expression, a hypothesis that asserts the current branch's condition is added, much as if the dependent if-then-else syntax had been used.
In the function argument to certain higher-order functions, the context of the function's body is enriched with assumptions about the argument.
This list is not exhaustive, and the mechanism is extensible. It is described in detail in the section on preprocessing.
Here, the termIfThenElse : term
`if c then t else e` is notation for `ite c t e`, "if-then-else", which decides to
return `t` or `e` depending on whether `c` is true or false. The explicit argument
`c : Prop` does not have any actual computational content, but there is an additional
`[Decidable c]` argument synthesized by typeclass inference which actually
determines how to evaluate `c` to true or false. Write `if h : c then t else e`
instead for a "dependent if-then-else" `dite`, which allows `t`/`e` to use the fact
that `c` is true/false.
if
does not add a local assumption about the condition (that is, whether n ≤ 1
) to the local contexts in the branches.
def fib (n : Nat) :=
if n ≤ 1 then
1
else
fib (n - 1) + fib (n - 2)
termination_by n
decreasing_by
n:Nath✝:¬n ≤ 1⊢ n - 1 < nn:Nath✝:¬n ≤ 1⊢ n - 2 < n
Nevertheless, the assumptions are available in the context of the termination proof:
Termination proof obligations in body of a Lean.Parser.Term.doFor : doElem
`for x in e do s` iterates over `e` assuming `e`'s type has an instance of the `ForIn` typeclass.
`break` and `continue` are supported inside `for` loops.
`for x in e, x2 in e2, ... do s` iterates of the given collections in parallel,
until at least one of them is exhausted.
The types of `e2` etc. must implement the `ToStream` typeclass.
for
…
Lean.Parser.Term.doFor : doElem
`for x in e do s` iterates over `e` assuming `e`'s type has an instance of the `ForIn` typeclass.
`break` and `continue` are supported inside `for` loops.
`for x in e, x2 in e2, ... do s` iterates of the given collections in parallel,
until at least one of them is exhausted.
The types of `e2` etc. must implement the `ToStream` typeclass.
in
loop are also enriched, in this case with a Std.Range
membership hypothesis:
def f (xs : Array Nat) : Nat := Id.run do
let mut s := xs.sum
for i in [:xs.size] do
s := s + f (xs.take i)
pure s
termination_by xs
decreasing_by
xs:Array Nati:Nath✝:i ∈ { stop := xs.size, step_pos := Nat.zero_lt_one }⊢ sizeOf (xs.take i) < sizeOf xs
Similarly, in the following (contrived) example, the termination proof contains an additional assumption showing that x ∈ xs
.
def f (n : Nat) (xs : List Nat) : Nat :=
List.sum (xs.map (fun x => f x []))
termination_by xs
decreasing_by
n:Natxs:List Natx:Nath✝:x ∈ xs⊢ sizeOf [] < sizeOf xs
This feature requires special setup for the higher-order function under which the recursive call is nested, as described in the section on preprocessing.
In the following definition, identical to the one above except using a custom, equivalent function instead of List.map
, the proof obligation context is not enriched:
def List.myMap := @List.map
def f (n : Nat) (xs : List Nat) : Nat :=
List.sum (xs.myMap (fun x => f x []))
termination_by xs
decreasing_by
n:Natxs:List Natx:Nat⊢ sizeOf [] < sizeOf xs
If no Lean.Parser.Command.declaration : command
decreasing_by
clause is given, then the decreasing_tactic
is used implicitly, and applied to each proof obligation separately.
decreasing_tactic
The tactic decreasing_tactic
mainly deals with lexicographic ordering of tuples, applying Prod.Lex.right
if the left components of the product are definitionally equal, and Prod.Lex.left
otherwise.
After preprocessing tuples this way, it calls the decreasing_trivial
tactic.
decreasing_trivial
Extensible helper tactic for decreasing_tactic
. This handles the "base case"
reasoning after applying lexicographic order lemmas.
It can be extended by adding more macro definitions, e.g.
macro_rules | `(tactic| decreasing_trivial) => `(tactic| linarith)
The tactic decreasing_trivial
is an extensible tactic that applies a few common heuristics to solve a termination goal.
In particular, it tries the following tactics and theorems:
theorems Nat.sub_succ_lt_self
, Nat.pred_lt_of_lt
, and Nat.pred_lt
, which handle common arithmetic goals
array_get_dec
and array_mem_dec
, which prove that the size of array elements is less than the size of the array
sizeOf_list_dec
that the size of list elements is less than the size of the list
String.Iterator.sizeOf_next_lt_of_hasNext
and String.Iterator.sizeOf_next_lt_of_atEnd
, to handle iteration through a string using Lean.Parser.Term.doFor : doElem
`for x in e do s` iterates over `e` assuming `e`'s type has an instance of the `ForIn` typeclass.
`break` and `continue` are supported inside `for` loops.
`for x in e, x2 in e2, ... do s` iterates of the given collections in parallel,
until at least one of them is exhausted.
The types of `e2` etc. must implement the `ToStream` typeclass.
for
This tactic is intended to be extended with further heuristics using Lean.Parser.Command.macro_rules : command
macro_rules
.
A classic example of a recursive function that needs a more complex measure is the Ackermann function:
def ack : Nat → Nat → Nat
| 0, n => n + 1
| m + 1, 0 => ack m 1
| m + 1, n + 1 => ack m (ack (m + 1) n)
termination_by m n => (m, n)
The measure is a tuple, so every recursive call has to be on arguments that are lexicographically smaller than the parameters.
The default decreasing_tactic
can handle this.
In particular, note that the third recursive call has a second argument that is smaller than the second parameter and a first argument that is definitionally equal to the first parameter.
This allowed decreasing_tactic
to apply Prod.Lex.right
.
Prod.Lex.right {α β} {ra : α → α → Prop} {rb : β → β → Prop}
(a : α) {b₁ b₂ : β}
(h : rb b₁ b₂) :
Prod.Lex ra rb (a, b₁) (a, b₂)
It fails, however, with the following modified function definition, where the third recursive call's first argument is provably smaller or equal to the first parameter, but not syntactically equal:
def synack : Nat → Nat → Nat
| 0, n => n + 1
| m + 1, 0 => synack m 1
| m + 1, n + 1 => synack m (synack (m / 2 + 1) n)
termination_by m n => (m, n)
failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goal case h m n : Nat ⊢ m / 2 + 1 < m + 1
Because Prod.Lex.right
is not applicable, the tactic used Prod.Lex.left
, which resulted in the unprovable goal above.
This function definition may require a manual proof that uses the more general theorem Prod.Lex.right'
, which allows the first component of the tuple (which must be of type Nat
) to be less or equal instead of strictly equal:
Prod.Lex.right' {β} (rb : β → β → Prop)
{a₂ : Nat} {b₂ : β} {a₁ : Nat} {b₁ : β}
(h₁ : a₁ ≤ a₂) (h₂ : rb b₁ b₂) :
Prod.Lex Nat.lt rb (a₁, b₁) (a₂, b₂)
def synack : Nat → Nat → Nat
| 0, n => n + 1
| m + 1, 0 => synack m 1
| m + 1, n + 1 => synack m (synack (m / 2 + 1) n)
termination_by m n => (m, n)
decreasing_by
m:Nat⊢ Prod.Lex (fun a₁ a₂ => a₁ < a₂) (fun a₁ a₂ => a₁ < a₂) (m, 1) (m.succ, 0) m:Nat⊢ m < m.succ
All goals completed! 🐙
-- the next goal corresponds to the third recursive call
m:Natn:Nat⊢ Prod.Lex (fun a₁ a₂ => a₁ < a₂) (fun a₁ a₂ => a₁ < a₂) (m / 2 + 1, n) (m.succ, n.succ) m:Natn:Nat⊢ m / 2 + 1 ≤ m.succm:Natn:Nat⊢ n < n.succ
m:Natn:Nat⊢ m / 2 + 1 ≤ m.succ All goals completed! 🐙
m:Natn:Nat⊢ n < n.succ All goals completed! 🐙
m:Natn:Natx✝:(y : (_ : Nat) ×' Nat) →
(invImage (fun x => PSigma.casesOn x fun a a_1 => (a, a_1)) Prod.instWellFoundedRelation).1 y ⟨m.succ, n.succ⟩ → Nat⊢ Prod.Lex (fun a₁ a₂ => a₁ < a₂) (fun a₁ a₂ => a₁ < a₂) (m, x✝ ⟨m / 2 + 1, n⟩ ⋯) (m.succ, n.succ) m:Natn:Natx✝:(y : (_ : Nat) ×' Nat) →
(invImage (fun x => PSigma.casesOn x fun a a_1 => (a, a_1)) Prod.instWellFoundedRelation).1 y ⟨m.succ, n.succ⟩ → Nat⊢ m < m.succ
All goals completed! 🐙
The decreasing_tactic
tactic does not use the stronger Prod.Lex.right'
because it would require backtracking on failure.
If a recursive function definition does not indicate a termination measure, Lean will attempt to discover one automatically.
If neither Lean.Parser.Command.declaration : command
termination_by
nor Lean.Parser.Command.declaration : command
decreasing_by
is provided, Lean will try to infer structural recursion before attempting well-founded recursion.
If a Lean.Parser.Command.declaration : command
decreasing_by
clause is present, only well-founded recursion is attempted.
To infer a suitable termination measure, Lean considers multiple basic termination measures, which are termination measures of type Nat
, and then tries all tuples of these measures.
The basic termination measures considered are:
all parameters whose type have a non-trivial SizeOf
instance
the expression e₂ - e₁
whenever the local context of a recursive call has an assumption of type e₁ < e₂
or e₁ ≤ e₂
, where e₁
and e₂
are of type Nat
and depend only on the function's parameters. This approach is based on work by Panagiotis Manolios and Daron Vroon, 2006. “Termination Analysis with Calling Context Graphs”. In Proceedings of the International Conference on Computer Aided Verification (CAV 2006). (LNCS 4144).
in a mutual group, an additional basic measure is used to distinguish between recursive calls to other functions in the group and recursive calls to the function being defined (for details, see the section on mutual well-founded recursion)
Candidate measures are basic measures or tuples of basic measures.
If any of the candidate measures allow all proof obligations to be discharged by the termination proof tactic (that is, the tactic specified by Lean.Parser.Command.declaration : command
decreasing_by
, or decreasing_trivial
if there is no Lean.Parser.Command.declaration : command
decreasing_by
clause), then an arbitrary such candidate measure is selected as the automatic termination measure.
A termination_by?
clause causes the inferred termination annotation to be shown.
It can be automatically added to the source file using the offered suggestion or code action.
To avoid the combinatorial explosion of trying all tuples of measures, Lean first tabulates all basic termination measures, determining whether the basic measure is decreasing, strictly decreasing, or non-decreasing. A decreasing measure is smaller for at least one recursive call and never increases at any recursive call, while a strictly decreasing measure is smaller at all recursive calls. A non-decreasing measure is one that the termination tactic could not show to be decreasing or strictly decreasing. A suitable tuple is chosen based on the table.This approach is based on Lukas Bulwahn, Alexander Krauss, and Tobias Nipkow, 2007. “Finding Lexicographic Orders for Termination Proofs in Isabelle/HOL”. In Proceedings of the International Conference on Theorem Proving in Higher Order Logics (TPHOLS 2007). (LNTCS 4732). This table shows up in the error message when no automatic measure could be found.
If there is no Lean.Parser.Command.declaration : command
termination_by
clause, Lean attempts to infer a measure for well-founded recursion.
If it fails, then it prints the table mentioned above.
In this example, the Lean.Parser.Command.declaration : command
decreasing_by
clause simply prevents Lean from also attempting structural recursion; this keeps the error message specific.
def f : (n m l : Nat) → Nat
| n+1, m+1, l+1 => [
f (n+1) (m+1) (l+1),
f (n+1) (m-1) (l),
f (n) (m+1) (l) ].sum
| _, _, _ => 0
decreasing_by all_goals decreasing_tactic
Could not find a decreasing measure. The basic measures relate at each recursive call as follows: (<, ≤, =: relation proved, ? all proofs failed, _: no proof attempted) n m l 1) 32:6-25 = = = 2) 33:6-23 = < _ 3) 34:6-23 < _ _ Please use `termination_by` to specify a decreasing measure.
The three recursive calls are identified by their source positions. This message conveys the following facts:
In the first recursive call, all arguments are (provably) equal to the parameters
In the second recursive call, the first argument is equal to the first parameter and the second argument is provably smaller than the second parameter. The third parameter was not checked for this recursive call, because it was not necessary to determine that no suitable termination argument exists.
In the third recursive call, the first argument decreases strictly, and the other arguments were not checked.
When termination proofs fail in this manner, a good technique to discover the problem is to explicitly indicate the expected termination argument using Lean.Parser.Command.declaration : command
termination_by
.
This will surface the messages from the failing tactic.
The purpose of considering expressions of the form e₂ - e₁
as measures is to support the common idiom of counting up to some upper bound, in particular when traversing arrays in possibly interesting ways.
In the following function, which performs binary search on a sorted array, this heuristic helps Lean to find the j - i
measure.
def binarySearch (x : Int) (xs : Array Int) : Option Nat :=
go 0 xs.size
where
go (i j : Nat) (hj : j ≤ xs.size := by omega) :=
if h : i < j then
let mid := (i + j) / 2
let y := xs[mid]
if x = y then
some mid
else if x < y then
go i mid
else
go (mid + 1) j
else
none
termination_by?
The fact that the inferred termination argument uses some arbitrary measure, rather than an optimal or minimal one, is visible in the inferred measure, which contains a redundant j
:
Try this: termination_by (j, j - i)
The tactic indicated by Lean.Parser.Command.declaration : command
decreasing_by
is used slightly differently when inferring the termination measure than it is in the actual termination proof.
During inference, it is applied to a single goal, attempting to prove <
or ≤
on Nat
.
During the termination proof, it is applied to many simultaneous goals (one per recursive call), and the goals may involve the lexicographic ordering of pairs.
A consequence is that a Lean.Parser.Command.declaration : command
decreasing_by
block that addresses goals individually and which works successfully with an explicit termination argument can cause inference of the termination measure to fail:
def ack : Nat → Nat → Nat
| 0, n => n + 1
| m + 1, 0 => ack m 1
| m + 1, n + 1 => ack m (ack (m + 1) n)
decreasing_by
· apply Prod.Lex.left
omega
· apply Prod.Lex.right
omega
· apply Prod.Lex.left
omega
It is advisable to always include a Lean.Parser.Command.declaration : command
termination_by
clause whenever an explicit Lean.Parser.Command.declaration : command
decreasing_by
proof is given.
Because decreasing_tactic
avoids the need to backtrack by being incomplete with regard to lexicographic ordering, Lean may infer a termination measure that leads to goals that the tactic cannot prove.
In this case, the error message is the one that results from the failing tactic rather than the one that results from being unable to find a measure.
This is what happens in notAck
:
def notAck : Nat → Nat → Nat
| 0, n => n + 1
| m + 1, 0 => notAck m 1
| m + 1, n + 1 => notAck m (notAck (m / 2 + 1) n)
decreasing_by all_goals All goals completed! 🐙
failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goal case h m n : Nat ⊢ m / 2 + 1 < m + 1
In this case, explicitly stating the termination measure helps.
Lean supports the definition of mutually recursive functions using well-founded recursion.
Mutual recursion may be introduced using a mutual block, but it also results from Lean.Parser.Term.letrec : term
let rec
expressions and Lean.Parser.Command.declaration : command
where
blocks.
The rules for mutual well-founded recursion are applied to a group of actually mutually recursive, lifted definitions, that results from the elaboration steps for mutual groups.
If any function in the mutual group has a Lean.Parser.Command.declaration : command
termination_by
or Lean.Parser.Command.declaration : command
decreasing_by
clause, well-founded recursion is attempted.
If a termination measure is specified using Lean.Parser.Command.declaration : command
termination_by
for any function in the mutual group, then all functions in the group must specify a termination measure, and they have to have the same type.
If no termination argument is specified, the termination argument is inferred, as described above. In the case of mutual recursion, a third class of basic measures is considered during inference, namely for each function in the mutual group the measure that is 1
for that function and 0
for the others. This allows Lean to order the functions so that some calls from one function to another are allowed even if the parameters do not decrease.
In the following mutual function definitions, the parameter does not decrease in the call from g
to f
.
Nonetheless, the definition is accepted due to the ordering imposed on the functions themselves by the additional basic measure.
mutual
def f : (n : Nat) → Nat
| 0 => 0
| n+1 => g n
termination_by?
def g (n : Nat) : Nat := (f n) + 1
termination_by?
end
The inferred termination argument for f
is:
Try this: termination_by n => (n, 0)
The inferred termination argument for g
is:
Try this: termination_by (n, 1)
Lean preprocesses the function's body before determining the proof obligations at each call site, transforming it into an equivalent definition that may include additional information. This preprocessing step is primarily used to enrich the local context with additional assumptions that may be necessary in order to solve the termination proof obligations, freeing users from the need to perform equivalent transformations by hand. Preprocessing uses the simplifier and is extensible by the user.
The preprocessing happens in three steps:
Lean annotates occurrences of a function's parameter, or a subterm of a parameter, with the wfParam
gadget.
wfParam {α} (a : α) : α
More precisely, every occurrence of the function's parameters is wrapped in wfParam
.
Whenever a Lean.Parser.Term.match : term
Pattern matching. `match e, ... with | p, ... => f | ...` matches each given
term `e` against each pattern `p` of a match alternative. When all patterns
of an alternative match, the `match` term evaluates to the value of the
corresponding right-hand side `f` with the pattern variables bound to the
respective matched values.
If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available
within `f`.
When not constructing a proof, `match` does not automatically substitute variables
matched on in dependent variables' types. Use `match (generalizing := true) ...` to
enforce this.
Syntax quotations can also be used in a pattern match.
This matches a `Syntax` value against quotations, pattern variables, or `_`.
Quoted identifiers only match identical identifiers - custom matching such as by the preresolved
names only should be done explicitly.
`Syntax.atom`s are ignored during matching by default except when part of a built-in literal.
For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they
should participate in matching.
For example, in
```lean
syntax "c" ("foo" <|> "bar") ...
```
`foo` and `bar` are indistinguishable during matching, but in
```lean
syntax foo := "foo"
syntax "c" (foo <|> "bar") ...
```
they are not.
match
expression has any discriminant wrapped in wfParam
, the gadget is removed and every occurrence of a pattern match variable (regardless of whether it comes from the discriminant with the wfParam
gadget) is wrapped in wfParam
.
The wfParam
gadget is additionally floated out of projection function applications.
The annotated function body is simplified using the simplifier, using only simplification rules from the wf_preprocess
custom simp set.
Finally, any left-over wfParam
markers are removed.
Annotating function parameters that are used for well-founded recursion allows the preprocessing simplification rules to distinguish between parameters and other terms.
attr ::= ...
| Theorems tagged with the `wf_preprocess` attribute are used during the processing of functions defined
by well-founded recursion. They are applied to the function's body to add additional hypotheses,
such as replacing `if c then _ else _` with `if h : c then _ else _` or `xs.map` with
`xs.attach.map`. Also see `wfParam`.
wf_preprocess
Theorems tagged with the wf_preprocess
attribute are used during the processing of functions defined
by well-founded recursion. They are applied to the function's body to add additional hypotheses,
such as replacing if c then _ else _
with if h : c then _ else _
or xs.map
with
xs.attach.map
. Also see wfParam
.
wfParam.{u} {α : Sort u} (a : α) : αwfParam.{u} {α : Sort u} (a : α) : α
The wfParam
gadget is used internally during the construction of recursive functions by
wellfounded recursion, to keep track of the parameter for which the automatic introduction
of List.attach
(or similar) is plausible.
Some rewrite rules in the wf_preprocess
simp set apply generally, without heeding the wfParam
marker.
In particular, the theorem ite_eq_dite
is used to extend the context of an if-then-else expression branch with an assumption about the condition:This assumption's name should be an inaccessible name based on h
, as is indicated by using binderNameHint
with the term ()
. Binder name hints are described in the tactic language reference.
ite_eq_dite {P : Prop} {α : Sort u} {a b : α} [Decidable P] :
(if P then a else b) =
if h : P then
binderNameHint h () a
else
binderNameHint h () b
Other rewrite rules use the wfParam
marker to restrict their applicability; they are used only when a function (like List.map
) is applied to a parameter or subterm of a parameter, but not otherwise.
This is typically done in two steps:
A theorem such as List.map_wfParam
recognizes a call of List.map
on a function parameter (or subterm), and uses List.attach
to enrich the type of the list elements with the assertion that they are indeed elements of that list:
List.map_wfParam (xs : List α) (f : α → β) :
(wfParam xs).map f = xs.attach.unattach.map f
A theorem such as List.map_unattach
makes that assertion available to the function parameter of List.map
.
List.map_unattach (P : α → Prop)
(xs : List { x : α // P x }) (f : α → β) :
xs.unattach.map f = xs.map fun ⟨x, h⟩ =>
binderNameHint x f <|
binderNameHint h () <|
f (wfParam x)
This theorem uses the binderNameHint
gadget to preserve a user-chosen binder name, should f
be a lambda expression.
By separating the introduction of List.attach
from the propagation of the introduced assumption, the desired the x ∈ xs
assumption is made available to f
even in chains such as (xs.reverse.filter p).map f
.
This preprocessing can be disabled by setting the option wf.preprocess
to false
.
To see the preprocessed function definition, before and after the removal of wfParam
markers, set the option trace.Elab.definition.wf
to true
.
trace.Elab.definition.wf
Default value: false
enable/disable tracing for the given module and submodules
This example demonstrates what is necessary to enable automatic well-founded recursion for a custom container type.
The structure type Pair
is a homogeneous pair: it contains precisely two elements, both of which have the same type.
It can be thought of as being similar to a list or array that always contains precisely two elements.
As a container, Pair
can support a map
operation.
To support well-founded recursion in which recursive calls occur in the body of a function being mapped over a Pair
, some additional definitions are required, including a membership predicate, a theorem that relates the size of a member to the size of the containing pair, helpers to introduce and eliminate assumptions about membership, wf_preprocess
rules to insert these helpers, and an extension to the decreasing_trivial
tactic.
Each of these steps makes it easier to work with Pair
, but none are strictly necessary; there's no need to immediately implement all steps for every type.
/-- A homogeneous pair -/
structure Pair (α : Type u) where
fst : α
snd : α
/-- Mapping a function over the elements of a pair -/
def Pair.map (f : α → β) (p : Pair α) : Pair β where
fst := f p.fst
snd := f p.snd
Defining a nested inductive data type of binary trees that uses Pair
and attempting to define its map
function demonstrates the need for preprocessing rules.
/-- A binary tree defined using `Pair` -/
inductive Tree (α : Type u) where
| leaf : α → Tree α
| node : Pair (Tree α) → Tree α
A straightforward definition of the map
function fails:
def Tree.map (f : α → β) : Tree α → Tree β
| leaf x => leaf (f x)
| node p => node (p.map (fun t' => t'.map f))
termination_by t => t
failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goal α : Type u_1 p : Pair (Tree α) t' : Tree α ⊢ sizeOf t' < 1 + sizeOf p
Clearly the proof obligation is not solvable, because nothing connects t'
to p
.
The standard idiom to enable this kind of function definition is to have a function that enriches each element of a collection with a proof that they are, in fact, elements of the collection. Stating this property requires a membership predicate.
inductive Pair.Mem (p : Pair α) : α → Prop where
| fst : Mem p p.fst
| snd : Mem p p.snd
instance : Membership α (Pair α) where
mem := Pair.Mem
Every inductive type automatically has a SizeOf
instance.
An element of a collection should be smaller than the collection, but this fact must be proved before it can be used to construct a termination proof:
theorem Pair.sizeOf_lt_of_mem {α} [SizeOf α]
{p : Pair α} {x : α} (h : x ∈ p) :
sizeOf x < sizeOf p := α:Type u_1inst✝:SizeOf αp:Pair αx:αh:x ∈ p⊢ sizeOf x < sizeOf p
α:Type u_1inst✝:SizeOf αp:Pair α⊢ sizeOf p.fst < sizeOf pα:Type u_1inst✝:SizeOf αp:Pair α⊢ sizeOf p.snd < sizeOf p α:Type u_1inst✝:SizeOf αp:Pair α⊢ sizeOf p.fst < sizeOf pα:Type u_1inst✝:SizeOf αp:Pair α⊢ sizeOf p.snd < sizeOf p α:Type u_1inst✝:SizeOf αfst✝:αsnd✝:α⊢ sizeOf { fst := fst✝, snd := snd✝ }.snd < sizeOf { fst := fst✝, snd := snd✝ } α:Type u_1inst✝:SizeOf αfst✝:αsnd✝:α⊢ sizeOf { fst := fst✝, snd := snd✝ }.fst < sizeOf { fst := fst✝, snd := snd✝ }α:Type u_1inst✝:SizeOf αfst✝:αsnd✝:α⊢ sizeOf { fst := fst✝, snd := snd✝ }.snd < sizeOf { fst := fst✝, snd := snd✝ } (α:Type u_1inst✝:SizeOf αfst✝:αsnd✝:α⊢ 0 < 1 + sizeOf fst✝; All goals completed! 🐙)
The next step is to define attach
and unattach
functions that enrich the elements of the pair with a proof that they are elements of the pair, or remove said proof.
Here, the type of Pair.unattach
is more general and can be used with any subtype; this is a typical pattern.
def Pair.attach (p : Pair α) : Pair {x : α // x ∈ p} where
fst := ⟨p.fst, .fst⟩
snd := ⟨p.snd, .snd⟩
def Pair.unattach {P : α → Prop} :
Pair {x : α // P x} → Pair α :=
Pair.map Subtype.val
Tree.map
can now be defined by using Pair.attach
and Pair.sizeOf_lt_of_mem
explicitly:
def Tree.map (f : α → β) : Tree α → Tree β
| leaf x => leaf (f x)
| node p => node (p.attach.map (fun ⟨t', _⟩ => t'.map f))
termination_by t => t
decreasing_by
α:Type u_1p:Pair (Tree α)t':Tree αproperty✝:t' ∈ pthis:sizeOf t' < sizeOf p⊢ sizeOf t' < sizeOf (node p)
α:Type u_1p:Pair (Tree α)t':Tree αproperty✝:t' ∈ pthis:sizeOf t' < sizeOf p⊢ sizeOf t' ≤ sizeOf p
All goals completed! 🐙
This transformation can be made fully automatic.
The preprocessing feature of well-founded recursion can be used to automate the introduction of the Pair.attach
function.
This is done in two stages.
First, when Pair.map
is applied to one of the function's parameters, it is rewritten to an attach
/unattach
composition.
Then, when a function is mapped over the result of Pair.unattach
, the function is rewritten to accept the proof of membership and bring it into scope.
@[wf_preprocess]
theorem Pair.map_wfParam (f : α → β) (p : Pair α) :
(wfParam p).map f = p.attach.unattach.map f := α:Type u_1β:Type u_2f:α → βp:Pair α⊢ map f (wfParam p) = map f p.attach.unattach
α:Type u_1β:Type u_2f:α → βfst✝:αsnd✝:α⊢ map f (wfParam { fst := fst✝, snd := snd✝ }) = map f { fst := fst✝, snd := snd✝ }.attach.unattach
All goals completed! 🐙
@[wf_preprocess]
theorem Pair.map_unattach {P : α → Prop}
(p : Pair (Subtype P)) (f : α → β) :
p.unattach.map f =
p.map fun ⟨x, h⟩ =>
binderNameHint x f <|
f (wfParam x) := α:Type u_1β:Type u_2P:α → Propp:Pair (Subtype P)f:α → β⊢ map f p.unattach =
map
(fun x =>
match x with
| ⟨x, h⟩ => binderNameHint x f (f (wfParam x)))
p
α:Type u_1β:Type u_2P:α → Propf:α → βfst✝:Subtype Psnd✝:Subtype P⊢ map f { fst := fst✝, snd := snd✝ }.unattach =
map
(fun x =>
match x with
| ⟨x, h⟩ => binderNameHint x f (f (wfParam x)))
{ fst := fst✝, snd := snd✝ }; All goals completed! 🐙
Now the function body can be written without extra considerations, and the membership assumption is still available to the termination proof.
def Tree.map (f : α → β) : Tree α → Tree β
| leaf x => leaf (f x)
| node p => node (p.map (fun t' => t'.map f))
termination_by t => t
decreasing_by
α:Type u_1p:Pair (Tree α)t':Tree αh:t' ∈ pthis:sizeOf t' < sizeOf p⊢ sizeOf t' < sizeOf (node p)
α:Type u_1p:Pair (Tree α)t':Tree αh:t' ∈ pthis:sizeOf t' < sizeOf p⊢ sizeOf t' < 1 + sizeOf p
All goals completed! 🐙
The proof can be made fully automatic by adding sizeOf_lt_of_mem
to the decreasing_trivial
tactic, as is done for similar built-in theorems.
macro "sizeOf_pair_dec" : tactic =>
`(tactic| with_reducible
have := Pair.sizeOf_lt_of_mem ‹_›
omega
done)
macro_rules | `(tactic| decreasing_trivial) => `(tactic| sizeOf_pair_dec)
def Tree.map (f : α → β) : Tree α → Tree β
| leaf x => leaf (f x)
| node p => node (p.map (fun t' => t'.map f))
termination_by t => t
To keep the example short, the sizeOf_pair_dec
tactic is tailored to this particular recursion pattern and isn't really general enough for a general-purpose container library.
It does, however, demonstrate that libraries can be just as convenient in practice as the container types in the standard library.
This section gives a very brief glimpse into the mathematical constructions that underlie termination proofs via well-founded recursion, which may surface occasionally.
The elaboration of functions defined by well-founded recursion is based on the WellFounded.fix
operator.
WellFounded.fix.{u, v} {α : Sort u} {C : α → Sort v} {r : α → α → Prop} (hwf : WellFounded r) (F : (x : α) → ((y : α) → r y x → C y) → C x) (x : α) : C xWellFounded.fix.{u, v} {α : Sort u} {C : α → Sort v} {r : α → α → Prop} (hwf : WellFounded r) (F : (x : α) → ((y : α) → r y x → C y) → C x) (x : α) : C x
A well-founded fixpoint. If satisfying the motive C
for all values that are smaller according to a
well-founded relation allows it to be satisfied for the current value, then it is satisfied for all
values.
This function is used as part of the elaboration of well-founded recursion.
The type α
is instantiated with the function's (varying) parameters, packed into one type using PSigma
.
The WellFounded
relation is constructed from the termination measure via invImage
.
invImage.{u_1, u_2} {α : Sort u_1} {β : Sort u_2} (f : α → β) (h : WellFoundedRelation β) : WellFoundedRelation αinvImage.{u_1, u_2} {α : Sort u_1} {β : Sort u_2} (f : α → β) (h : WellFoundedRelation β) : WellFoundedRelation α
The inverse image of a well-founded relation is well-founded.
The function's body is passed to WellFounded.fix
, with parameters suitably packed and unpacked, and recursive calls are replaced with a call to the value provided by WellFounded.fix
.
The termination proofs generated by the Lean.Parser.Command.declaration : command
decreasing_by
tactics are inserted in the right place.
Finally, the equational and unfolding theorems for the recursive function are proved from WellFounded.fix_eq
.
These theorems hide the details of packing and unpacking arguments and describe the function's behavior in terms of the original definition.
In the case of mutual recursion, an equivalent non-mutual function is constructed by combining the function's arguments using PSum
, and pattern-matching on that sum type in the result type and the body.
The definition of WellFounded
builds on the notion of accessible elements of the relation:
WellFounded.{u} {α : Sort u} (r : α → α → Prop) : PropWellFounded.{u} {α : Sort u} (r : α → α → Prop) : Prop
A relation r
is WellFounded
if all elements of α
are accessible within r
.
If a relation is WellFounded
, it does not allow for an infinite descent along the relation.
If the arguments of the recursive calls in a function definition decrease according to a well founded relation, then the function terminates. Well-founded relations are sometimes called Artinian or said to satisfy the “descending chain condition”.
intro.{u} {α : Sort u} {r : α → α → Prop} (h : ∀ (a : α), Acc r a) : WellFounded r
If all elements are accessible via r
, then r
is well-founded.
Acc.{u} {α : Sort u} (r : α → α → Prop) : α → PropAcc.{u} {α : Sort u} (r : α → α → Prop) : α → Prop
The definition of division by iterated subtraction can be written explicitly using well-founded recursion.
noncomputable def div (n k : Nat) : Nat :=
(inferInstanceAs (WellFoundedRelation Nat)).wf.fix
(fun n r =>
if h : k = 0 then 0
else if h : k > n then 0
else 1 + (r (n - k) <| α:Type un✝:Natk:Natn:Natr:(y : Nat) → WellFoundedRelation.rel y n → Nath✝:¬k = 0h:¬k > n⊢ WellFoundedRelation.rel (n - k) n
α:Type un✝:Natk:Natn:Natr:(y : Nat) → WellFoundedRelation.rel y n → Nath✝:¬k = 0h:¬k > n⊢ n - k < n
All goals completed! 🐙))
n
The definition must be marked Lean.Parser.Command.declaration : command
noncomputable
because well-founded recursion is not supported by the compiler.
Like recursors, it is part of Lean's logic.
The definition of division should satisfy the following equations:
∀{n k : Nat}, (k = 0) → div n k = 0
∀{n k : Nat}, (k > n) → div n k = 0
∀{n k : Nat}, (k ≠ 0) → (¬ k > n) → div n k = div (n - k) k
This reduction behavior does not hold definitionally:
theorem div.eq0 : div n 0 = 0 := n:Nat⊢ div n 0 = 0 n:Nat⊢ div n 0 = 0
tactic 'rfl' failed, the left-hand side div n 0 is not definitionally equal to the right-hand side 0 n : Nat ⊢ div n 0 = 0
However, using WellFounded.fix_eq
to unfold the well-founded recursion, the three equations can be proved to hold:
theorem div.eq0 : div n 0 = 0 := n:Nat⊢ div n 0 = 0
n:Nat⊢ _proof_2.fix (fun n r => if h : 0 = 0 then 0 else if h : 0 > n then 0 else 1 + r (n - 0) ⋯) n = 0
All goals completed! 🐙
theorem div.eq1 : k > n → div n k = 0 := k:Natn:Nat⊢ k > n → div n k = 0
k:Natn:Nath:k > n⊢ div n k = 0
k:Natn:Nath:k > n⊢ _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ⋯) n = 0
k:Natn:Nath:k > n⊢ (if h : k = 0 then 0
else
if h : k > n then 0
else
1 +
(fun y x => _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ⋯) y)
(n - k) ⋯) =
0
k:Natn:Nath:k > n⊢ ¬k = 0 →
k ≤ n → 1 + _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : n < k then 0 else 1 + r (n - k) ⋯) (n - k) = 0
k:Natn:Nath:k > na✝¹:¬k = 0a✝:k ≤ n⊢ 1 + _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : n < k then 0 else 1 + r (n - k) ⋯) (n - k) = 0; All goals completed! 🐙
theorem div.eq2 :
¬ k = 0 → ¬ (k > n) →
div n k = 1 + div (n - k) k := k:Natn:Nat⊢ ¬k = 0 → ¬k > n → div n k = 1 + div (n - k) k
k:Natn:Nata✝¹:¬k = 0a✝:¬k > n⊢ div n k = 1 + div (n - k) k
k:Natn:Nata✝¹:¬k = 0a✝:¬k > n⊢ _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ⋯) n =
1 + _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ⋯) (n - k)
k:Natn:Nata✝¹:¬k = 0a✝:¬k > n⊢ (if h : k = 0 then 0
else
if h : k > n then 0
else
1 +
(fun y x => _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ⋯) y)
(n - k) ⋯) =
1 + _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ⋯) (n - k)
k:Natn:Nata✝¹:¬k = 0a✝:k ≤ n⊢ n < k → 0 = 1 + _proof_2.fix (fun n r => if h : n < k then 0 else 1 + r (n - k) ⋯) (n - k)
All goals completed! 🐙
All definitions are fundamentally equations: the new constant being defined is equal to the right-hand side of the definition. For functions defined by structural recursion, this equation holds definitionally, and there is a unique value returned by application of the function. For functions defined by well-founded recursion, the equation may hold only propositionally, but all type-correct applications of the function to arguments are equal to the respective values prescribed by the definition. In both cases, the fact that the function terminates for all inputs means that the value computed by applying the function is always uniquely determined.
In some cases where a function does not terminate for all arguments, the equation may not uniquely determine the function's return value for each input, but there are nonetheless functions for which the defining equation holds. In these cases, a definition as a partial fixpoint may still be possible. Any function that satisfies the defining equation can be used to demonstrate that the equation does not create a logical contradiction, and the equation can then be proven as a theorem about this function. As with the other strategies for defining recursive functions, compiled code uses the function as it was originally written; like definitions in terms of eliminators or recursion over accessibility proofs, the function used to define the partial fixpoint is used only to justify the function's equations in Lean's logic for purposes of mathematical reasoning.
The term partial fixpoint is specific to Lean.
Functions declared Lean.Parser.Command.declaration : command
partial
do not require termination proofs, so long as the type of their return values is inhabited, but they are completely opaque from the perspective of Lean's logic.
Partial fixpoints, on the other hand, can be rewritten using their defining equations while writing proofs.
Logically speaking, partial fixpoints are total functions that don't reduce definitionally when applied, but for which equational rewrite rule are provided.
They are partial in the sense that the defining equation does not necessarily specify a value for all possible arguments.
While partial fixpoints do allow functions to be defined that cannot be expressed using structural or well-founded recursion, the technique is also useful in other cases. Even in cases where the defining equation fully describes the function's behavior and a termination proof using well-founded recursion would be possible, it may simply be more convenient to define the function as a partial fixpoint to avoid a having to write a termination proof.
Defining recursive functions as partial fixpoints only occurs when explicitly requested by annotating the definition with Lean.Parser.Command.declaration : command
partial_fixpoint
.
There are two classes of functions that can be defined as partial fixpoints:
Tail-recursive functions whose return type is inhabited type
Functions that return values in a suitable monad, such as the Option
monad
Both classes are backed by the same theory and construction: least fixpoints of monotone equations in chain-complete partial orders.
Just as with structural and well-founded recursion, Lean allows mutually recursive functions to be defined as partial fixpoints.
To use this feature, every function definition in a mutual block must be annotated with the Lean.Parser.Command.declaration : command
partial_fixpoint
modifier.
The following function finds the least natural number for which the predicate p
holds.
If p
never holds, then this equation does not specify the behavior: the function find
could return 42
or any other Nat
in that case and still satisfy the equation.
def find (p : Nat → Bool) (i : Nat := 0) : Nat :=
if p i then
i
else
find p (i + 1)
partial_fixpoint
The elaborator can prove that functions satisfying the equation exist.
Within Lean's logic, find
is defined to be an arbitrary such function.
A recursive function can be defined as a partial fixpoint if the following two conditions hold:
The function's return type is inhabited (as with functions marked Lean.Parser.Command.declaration : command
partial
) - either a Nonempty
or Inhabited
instance works.
All recursive calls are in tail position of the function.
An expression is in tail position in the function body if it is:
the function body itself,
the branches of a Lean.Parser.Term.match : term
Pattern matching. `match e, ... with | p, ... => f | ...` matches each given
term `e` against each pattern `p` of a match alternative. When all patterns
of an alternative match, the `match` term evaluates to the value of the
corresponding right-hand side `f` with the pattern variables bound to the
respective matched values.
If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available
within `f`.
When not constructing a proof, `match` does not automatically substitute variables
matched on in dependent variables' types. Use `match (generalizing := true) ...` to
enforce this.
Syntax quotations can also be used in a pattern match.
This matches a `Syntax` value against quotations, pattern variables, or `_`.
Quoted identifiers only match identical identifiers - custom matching such as by the preresolved
names only should be done explicitly.
`Syntax.atom`s are ignored during matching by default except when part of a built-in literal.
For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they
should participate in matching.
For example, in
```lean
syntax "c" ("foo" <|> "bar") ...
```
`foo` and `bar` are indistinguishable during matching, but in
```lean
syntax foo := "foo"
syntax "c" (foo <|> "bar") ...
```
they are not.
match
expression that is in tail position,
the branches of an termIfThenElse : term
`if c then t else e` is notation for `ite c t e`, "if-then-else", which decides to
return `t` or `e` depending on whether `c` is true or false. The explicit argument
`c : Prop` does not have any actual computational content, but there is an additional
`[Decidable c]` argument synthesized by typeclass inference which actually
determines how to evaluate `c` to true or false. Write `if h : c then t else e`
instead for a "dependent if-then-else" `dite`, which allows `t`/`e` to use the fact
that `c` is true/false.
if
expression that is in tail position, and
the body of a Lean.Parser.Term.let : term
`let` is used to declare a local definition. Example:
```
let x := 1
let y := x + 1
x + y
```
Since functions are first class citizens in Lean, you can use `let` to declare
local functions too.
```
let double := fun x => 2*x
double (double 3)
```
For recursive definitions, you should use `let rec`.
You can also perform pattern matching using `let`. For example,
assume `p` has type `Nat × Nat`, then you can write
```
let (x, y) := p
x + y
```
let
expression that is in tail position.
In particular, the discriminant of a Lean.Parser.Term.match : term
Pattern matching. `match e, ... with | p, ... => f | ...` matches each given
term `e` against each pattern `p` of a match alternative. When all patterns
of an alternative match, the `match` term evaluates to the value of the
corresponding right-hand side `f` with the pattern variables bound to the
respective matched values.
If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available
within `f`.
When not constructing a proof, `match` does not automatically substitute variables
matched on in dependent variables' types. Use `match (generalizing := true) ...` to
enforce this.
Syntax quotations can also be used in a pattern match.
This matches a `Syntax` value against quotations, pattern variables, or `_`.
Quoted identifiers only match identical identifiers - custom matching such as by the preresolved
names only should be done explicitly.
`Syntax.atom`s are ignored during matching by default except when part of a built-in literal.
For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they
should participate in matching.
For example, in
```lean
syntax "c" ("foo" <|> "bar") ...
```
`foo` and `bar` are indistinguishable during matching, but in
```lean
syntax foo := "foo"
syntax "c" (foo <|> "bar") ...
```
they are not.
match
expression, the condition of an termIfThenElse : term
`if c then t else e` is notation for `ite c t e`, "if-then-else", which decides to
return `t` or `e` depending on whether `c` is true or false. The explicit argument
`c : Prop` does not have any actual computational content, but there is an additional
`[Decidable c]` argument synthesized by typeclass inference which actually
determines how to evaluate `c` to true or false. Write `if h : c then t else e`
instead for a "dependent if-then-else" `dite`, which allows `t`/`e` to use the fact
that `c` is true/false.
if
expression and the arguments of functions are not tail positions.
Because the function body itself is a tail position, the infinitely looping function loop
is tail recursive.
It can be defined as a partial fixpoint.
def loop (x : Nat) : Nat := loop (x + 1)
partial_fixpoint
Array.find
could also be constructed using well-founded recursion with a termination proof, but may be more convenient to define using Lean.Parser.Command.declaration : command
partial_fixpoint
, where no termination proof is needed.
def Array.find (xs : Array α) (p : α → Bool)
(i : Nat := 0) : Option α :=
if h : i < xs.size then
if p xs[i] then
some xs[i]
else
Array.find xs p (i + 1)
else
none
partial_fixpoint
If the result of the recursive call is not just returned, but passed to another function, it is not in tail position and this definition fails.
def List.findIndex (xs : List α) (p : α → Bool) : Int :=
match xs with
| [] => -1
| x::ys =>
if p x then
0
else
have r := List.findIndex ys p
if r = -1 then -1 else r + 1
partial_fixpoint
The error message on the recursive call is:
Could not prove 'List.findIndex' to be monotone in its recursive calls: Cannot eliminate recursive call `List.findIndex ys p` enclosed in have r := ys✝.findIndex p; if r = -1 then -1 else r + 1
Defining a function as a partial fixpoint is more powerful if the function's return type is a monad that is an instance of Lean.Order.MonoBind
, such as Option
.
In this case, recursive call are not restricted to tail-positions, but may also occur inside higher-order monadic functions such as bind
and List.mapM
.
The set of higher-order functions for which this works is extensible, so no exhaustive list is given here.
The aspiration is that a monadic recursive function definition that is built using abstract monadic operations like bind
, but that does not open the abstraction of the monad (e.g. by matching on the Option
value), is accepted.
In particular, using Lean.Parser.Term.do : term
do
-notation should work.
The following function implements the Ackermann function in the Option
monad, and is accepted without an (explicit or implicit) termination proof:
def ack : (n m : Nat) → Option Nat
| 0, y => some (y+1)
| x+1, 0 => ack x 1
| x+1, y+1 => do ack x (← ack (x+1) y)
partial_fixpoint
Recursive calls may also occur within higher-order functions such as List.mapM
, if they are set up appropriately, and Lean.Parser.Term.do : term
do
-notation:
structure Tree where cs : List Tree
def Tree.rev (t : Tree) : Option Tree := do
Tree.mk (← t.cs.reverse.mapM (Tree.rev ·))
partial_fixpoint
def Tree.rev' (t : Tree) : Option Tree := do
let mut cs := []
for c in t.cs do
cs := (← c.rev') :: cs
return Tree.mk cs
partial_fixpoint
Pattern matching on the result of the recursive call will prevent the definition by partial fixpoint from going through:
def List.findIndex (xs : List α) (p : α → Bool) : Option Nat :=
match xs with
| [] => none
| x::ys =>
if p x then
some 0
else
match List.findIndex ys p with
| none => none
| some r => some (r + 1)
partial_fixpoint
Could not prove 'List.findIndex' to be monotone in its recursive calls: Cannot eliminate recursive call `List.findIndex ys p` enclosed in match ys✝.findIndex p with | none => none | some r => some (r + 1)
In this particular case, using Functor.map
instead of explicit pattern matching helps:
def List.findIndex (xs : List α) (p : α → Bool) : Option Nat :=
match xs with
| [] => none
| x::ys =>
if p x then
some 0
else
(· + 1) <$> List.findIndex ys p
partial_fixpoint
For every function defined as a partial fixpoint, Lean proves that the defining equation is satisfied. This enables proofs by rewriting. However, these equational theorems are not sufficient for reasoning about the behavior of the function on arguments for which the function specification does not terminate. Code paths that lead to infinite recursion at runtime would end up as infinite chains of rewrites in a potential proof.
Partial fixpoints in suitable monads, on the other hand, provide additional theorems that map the undefined values from non-termination to suitable values in the monad.
In the Option
monad, then partial fixpoint equals Option.none
on all function inputs for which the defining equation specifies non-termination.
From this fact, Lean proves a partial correctness theorem for the function which allows facts to be concluded when the function's result is Option.some
.
Recall List.findIndex
from an earlier example:
def List.findIndex (xs : List α) (p : α → Bool) : Option Nat :=
match xs with
| [] => none
| x::ys =>
if p x then
some 0
else
(· + 1) <$> List.findIndex ys p
partial_fixpoint
With this function definition, Lean automatically proves the following partial correctness theorem:
List.findIndex.partial_correctness.{u_1} {α : Type u_1}
(p : α → Bool)
(motive : List α → Nat → Prop)
(h :
∀ (findIndex : List α → Option Nat),
(∀ (xs : List α) (r : Nat), findIndex xs = some r → motive xs r) →
∀ (xs : List α) (r : Nat),
(match xs with
| [] => none
| x :: ys =>
if p x = true then some 0
else (fun x => x + 1) <$> findIndex ys) = some r →
motive xs r)
(xs : List α) (r : Nat) :
xs.findIndex p = some r →
motive xs r
Here, the motive is a relation between the parameter and return types of List.findIndex
, with the Option
removed from the return type.
If, when given an arbitrary partial function with a signature that's compatible with List.findIndex
, the following hold:
the motive is satisfied for all inputs for which the arbitrary function returns a value (rather than none
),
taking one rewriting step with the defining equation, in which the recursive calls are replaced by the arbitrary function, also implies the satisfaction of the motive
then the motive is satsified for all inputs for which the List.findIndex
returns some
.
The partial correctness theorem is a reasoning principle. It can be used to prove that the resulting number is a valid index in the list and that the predicate holds for that index:
theorem List.findIndex_implies_pred
(xs : List α) (p : α → Bool) :
xs.findIndex p = some i →
∃x, xs[i]? = some x ∧ p x := α:Type u_1i:Natxs:List αp:α → Bool⊢ xs.findIndex p = some i → ∃ x, xs[i]? = some x ∧ p x = true
α:Type u_1i:Natxs:List αp:α → Bool⊢ ∀ (findIndex : List α → Option Nat),
(∀ (xs : List α) (r : Nat), findIndex xs = some r → ∃ x, xs[r]? = some x ∧ p x = true) →
∀ (xs : List α) (r : Nat),
(match xs with
| [] => none
| x :: ys => if p x = true then some 0 else (fun x => x + 1) <$> findIndex ys) =
some r →
∃ x, xs[r]? = some x ∧ p x = true
α:Type u_1i:Natxs✝:List αp:α → BoolfindIndex:List α → Option Natih:∀ (xs : List α) (r : Nat), findIndex xs = some r → ∃ x, xs[r]? = some x ∧ p x = truexs:List αr:Nathsome:(match xs with
| [] => none
| x :: ys => if p x = true then some 0 else (fun x => x + 1) <$> findIndex ys) =
some r⊢ ∃ x, xs[r]? = some x ∧ p x = true
α:Type u_1i:Natxs:List αp:α → BoolfindIndex:List α → Option Natih:∀ (xs : List α) (r : Nat), findIndex xs = some r → ∃ x, xs[r]? = some x ∧ p x = truer:Natxs✝:List αhsome:none = some r⊢ ∃ x, [][r]? = some x ∧ p x = trueα:Type u_1i:Natxs:List αp:α → BoolfindIndex:List α → Option Natih:∀ (xs : List α) (r : Nat), findIndex xs = some r → ∃ x, xs[r]? = some x ∧ p x = truer:Natxs✝:List αx✝:αys✝:List αhsome:(if p x✝ = true then some 0 else (fun x => x + 1) <$> findIndex ys✝) = some r⊢ ∃ x, (x✝ :: ys✝)[r]? = some x ∧ p x = true
next α:Type u_1i:Natxs:List αp:α → BoolfindIndex:List α → Option Natih:∀ (xs : List α) (r : Nat), findIndex xs = some r → ∃ x, xs[r]? = some x ∧ p x = truer:Natxs✝:List αhsome:none = some r⊢ ∃ x, [][r]? = some x ∧ p x = true All goals completed! 🐙
next x ys α:Type u_1i:Natxs:List αp:α → BoolfindIndex:List α → Option Natih:∀ (xs : List α) (r : Nat), findIndex xs = some r → ∃ x, xs[r]? = some x ∧ p x = truer:Natxs✝:List αx:αys:List αhsome:(if p x = true then some 0 else (fun x => x + 1) <$> findIndex ys) = some r⊢ ∃ x_1, (x :: ys)[r]? = some x_1 ∧ p x_1 = true
α:Type u_1i:Natxs:List αp:α → BoolfindIndex:List α → Option Natih:∀ (xs : List α) (r : Nat), findIndex xs = some r → ∃ x, xs[r]? = some x ∧ p x = truer:Natxs✝:List αx:αys:List αh✝:p x = truehsome:some 0 = some r⊢ ∃ x_1, (x :: ys)[r]? = some x_1 ∧ p x_1 = trueα:Type u_1i:Natxs:List αp:α → BoolfindIndex:List α → Option Natih:∀ (xs : List α) (r : Nat), findIndex xs = some r → ∃ x, xs[r]? = some x ∧ p x = truer:Natxs✝:List αx:αys:List αh✝:¬p x = truehsome:(fun x => x + 1) <$> findIndex ys = some r⊢ ∃ x_1, (x :: ys)[r]? = some x_1 ∧ p x_1 = true
next α:Type u_1i:Natxs:List αp:α → BoolfindIndex:List α → Option Natih:∀ (xs : List α) (r : Nat), findIndex xs = some r → ∃ x, xs[r]? = some x ∧ p x = truer:Natxs✝:List αx:αys:List αh✝:p x = truehsome:some 0 = some r⊢ ∃ x_1, (x :: ys)[r]? = some x_1 ∧ p x_1 = true
have : r = 0 := α:Type u_1i:Natxs:List αp:α → Bool⊢ xs.findIndex p = some i → ∃ x, xs[i]? = some x ∧ p x = true All goals completed! 🐙
All goals completed! 🐙
next α:Type u_1i:Natxs:List αp:α → BoolfindIndex:List α → Option Natih:∀ (xs : List α) (r : Nat), findIndex xs = some r → ∃ x, xs[r]? = some x ∧ p x = truer:Natxs✝:List αx:αys:List αh✝:¬p x = truehsome:(fun x => x + 1) <$> findIndex ys = some r⊢ ∃ x_1, (x :: ys)[r]? = some x_1 ∧ p x_1 = true
α:Type u_1i:Natxs:List αp:α → BoolfindIndex:List α → Option Natih:∀ (xs : List α) (r : Nat), findIndex xs = some r → ∃ x, xs[r]? = some x ∧ p x = truer:Natxs✝:List αx:αys:List αh✝:¬p x = truehsome:∃ a, findIndex ys = some a ∧ a + 1 = r⊢ ∃ x_1, (x :: ys)[r]? = some x_1 ∧ p x_1 = true
α:Type u_1i:Natxs:List αp:α → BoolfindIndex:List α → Option Natih:∀ (xs : List α) (r : Nat), findIndex xs = some r → ∃ x, xs[r]? = some x ∧ p x = truexs✝:List αx:αys:List αh✝:¬p x = truer':Nathr:findIndex ys = some r'⊢ ∃ x_1, (x :: ys)[r' + 1]? = some x_1 ∧ p x_1 = true
α:Type u_1i:Natxs:List αp:α → BoolfindIndex:List α → Option Natxs✝:List αx:αys:List αh✝:¬p x = truer':Nathr:findIndex ys = some r'ih:∃ x, ys[r']? = some x ∧ p x = true⊢ ∃ x_1, (x :: ys)[r' + 1]? = some x_1 ∧ p x_1 = true
All goals completed! 🐙
Lean supports the definition of mutually recursive functions using partial fixpoint.
Mutual recursion may be introduced using a mutual block, but it also results from Lean.Parser.Term.letrec : term
let rec
expressions and Lean.Parser.Command.declaration : command
where
blocks.
The rules for mutual well-founded recursion are applied to a group of actually mutually recursive, lifted definitions, that results from the elaboration steps for mutual groups.
If all functions in the mutual group have the Lean.Parser.Command.declaration : command
partial_fixpoint
clause, then this strategy is used.
The construction builds on a variant of the Knaster–Tarski theorem: In a chain-complete partial order, every monotone function has a least fixed point.
The necessary theory is found in the Lean.Order
namespace.
This is not meant to be a general purpose library of order theoretic results.
Instead, the definitions and theorems in Lean.Order
are only intended as implementation details of the Lean.Parser.Command.declaration : command
partial_fixpoint
feature, and they should be considered a private API that may change without notice.
The notion of a partial order, and that of a chain-complete partial order, are represented by the type classes Lean.Order.PartialOrder
and Lean.Order.CCPO
, respectively.
Lean.Order.PartialOrder.{u} (α : Sort u) : Sort (max 1 u)Lean.Order.PartialOrder.{u} (α : Sort u) : Sort (max 1 u)
A partial order is a reflexive, transitive and antisymmetric relation.
This is intended to be used in the construction of partial_fixpoint
, and not meant to be used otherwise.
Lean.Order.PartialOrder.mk.{u}
rel : α → α → Prop
A “less-or-equal-to” or “approximates” relation.
This is intended to be used in the construction of partial_fixpoint
, and not meant to be used otherwise.
rel_refl : ∀ {x : α}, x ⊑ x
The “less-or-equal-to” or “approximates” relation is reflexive.
rel_trans : ∀ {x y z : α}, x ⊑ y → y ⊑ z → x ⊑ z
The “less-or-equal-to” or “approximates” relation is transitive.
rel_antisymm : ∀ {x y : α}, x ⊑ y → y ⊑ x → x = y
The “less-or-equal-to” or “approximates” relation is antisymmetric.
Lean.Order.CCPO.{u} (α : Sort u) : Sort (max 1 u)Lean.Order.CCPO.{u} (α : Sort u) : Sort (max 1 u)
A chain-complete partial order (CCPO) is a partial order where every chain has a least upper bound.
This is intended to be used in the construction of partial_fixpoint
, and not meant to be used
otherwise.
Lean.Order.CCPO.mk.{u}
rel : α → α → Prop
rel_refl : ∀ {x : α}, x ⊑ x
rel_trans : ∀ {x y z : α}, x ⊑ y → y ⊑ z → x ⊑ z
rel_antisymm : ∀ {x y : α}, x ⊑ y → y ⊑ x → x = y
csup : (α → Prop) → α
The least upper bound of a chain.
This is intended to be used in the construction of partial_fixpoint
, and not meant to be used
otherwise.
csup_spec : ∀ {x : α} {c : α → Prop}, chain c → (CCPO.csup c ⊑ x ↔ ∀ (y : α), c y → y ⊑ x)
csup c
is the least upper bound of the chain c
when all elements x
that are at
least as large as csup c
are at least as large as all elements of c
, and vice versa.
A function is monotone if it preserves partial orders.
That is, if x ⊑ y
then f x ⊑ f y
.
The operator ⊑
represent Lean.Order.PartialOrder.rel
.
Lean.Order.monotone.{u, v} {α : Sort u} [PartialOrder α] {β : Sort v} [PartialOrder β] (f : α → β) : PropLean.Order.monotone.{u, v} {α : Sort u} [PartialOrder α] {β : Sort v} [PartialOrder β] (f : α → β) : Prop
A function is monotone if it maps related elements to related elements.
This is intended to be used in the construction of partial_fixpoint
, and not meant to be used otherwise.
The fixpoint of a monotone function can be taken using fix
, which indeed constructs a fixpoint, as shown by fix_eq
,
The least fixpoint of a monotone function is the least upper bound of its transfinite iteration.
The monotone f
assumption is not strictly necessarily for the definition, but without this the
definition is not very meaningful and it simplifies applying theorems like fix_eq
if every use of
fix
already has the monotonicty requirement.
This is intended to be used in the construction of partial_fixpoint
, and not meant to be used otherwise.
The main fixpoint theorem for fixedpoints of monotone functions in chain-complete partial orders.
This is intended to be used in the construction of partial_fixpoint
, and not meant to be used otherwise.
To construct the partial fixpoint, Lean first synthesizes a suitable CCPO
instance.
If the function's result type has a dedicated instance, like Option
has with instCCPOOption
, this is used together with the instance for the function type, instCCPOPi
, to construct an instance for the whole function's type.
Otherwise, if the function's type can be shown to be inhabited by a witness w
, then the instance FlatOrder.instCCPO
for the wrapper type FlatOrder w
is used. In this order, w
is a least element and all other elements are incomparable.
Next, the recursive calls in the right-hand side of the function definitions are abstracted; this turns into the argument f
of fix
. The monotonicity requirement is solved by the monotonicity
tactic, which applies compositional monotonicity lemmas in a syntax-driven way.
The tactic solves goals of the form monotone (fun x => … x …)
using the following steps:
Applying monotone_const
when there is no dependency on x
left.
Splitting on Lean.Parser.Term.match : term
Pattern matching. `match e, ... with | p, ... => f | ...` matches each given
term `e` against each pattern `p` of a match alternative. When all patterns
of an alternative match, the `match` term evaluates to the value of the
corresponding right-hand side `f` with the pattern variables bound to the
respective matched values.
If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available
within `f`.
When not constructing a proof, `match` does not automatically substitute variables
matched on in dependent variables' types. Use `match (generalizing := true) ...` to
enforce this.
Syntax quotations can also be used in a pattern match.
This matches a `Syntax` value against quotations, pattern variables, or `_`.
Quoted identifiers only match identical identifiers - custom matching such as by the preresolved
names only should be done explicitly.
`Syntax.atom`s are ignored during matching by default except when part of a built-in literal.
For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they
should participate in matching.
For example, in
```lean
syntax "c" ("foo" <|> "bar") ...
```
`foo` and `bar` are indistinguishable during matching, but in
```lean
syntax foo := "foo"
syntax "c" (foo <|> "bar") ...
```
they are not.
match
expressions.
Splitting on termIfThenElse : term
`if c then t else e` is notation for `ite c t e`, "if-then-else", which decides to
return `t` or `e` depending on whether `c` is true or false. The explicit argument
`c : Prop` does not have any actual computational content, but there is an additional
`[Decidable c]` argument synthesized by typeclass inference which actually
determines how to evaluate `c` to true or false. Write `if h : c then t else e`
instead for a "dependent if-then-else" `dite`, which allows `t`/`e` to use the fact
that `c` is true/false.
if
expressions.
Moving Lean.Parser.Term.let : term
`let` is used to declare a local definition. Example:
```
let x := 1
let y := x + 1
x + y
```
Since functions are first class citizens in Lean, you can use `let` to declare
local functions too.
```
let double := fun x => 2*x
double (double 3)
```
For recursive definitions, you should use `let rec`.
You can also perform pattern matching using `let`. For example,
assume `p` has type `Nat × Nat`, then you can write
```
let (x, y) := p
x + y
```
let
expression to the context, if the value and type do not depend on x
.
Zeta-reducing a Lean.Parser.Term.let : term
`let` is used to declare a local definition. Example:
```
let x := 1
let y := x + 1
x + y
```
Since functions are first class citizens in Lean, you can use `let` to declare
local functions too.
```
let double := fun x => 2*x
double (double 3)
```
For recursive definitions, you should use `let rec`.
You can also perform pattern matching using `let`. For example,
assume `p` has type `Nat × Nat`, then you can write
```
let (x, y) := p
x + y
```
let
expression when value and type do depend on x
.
Applying lemmas annotated with partial_fixpoint_monotone
The following monotonicity lemmas are registered, and should allow recursive calls under the given higher-order functions in the arguments indicated by ·
(but not the other arguments, shown as _
).
Theorem | Pattern |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
大多数 Lean 函数既可在 Lean 的类型论中进行推理,也可被编译并运行;但凡被标记为 partial
或 unsafe
的定义,则无法在逻辑层面进行有意义的推理。
从逻辑视角看,partial
函数是不透明常量;而凡是引用 unsafe
定义的定理都会被直接拒绝。
作为无法用于推理的交换条件,这些定义受到的约束大幅减少:这使得一些原本不切实际或成本过高而难以给出证明的程序仍然可以编写,同时又不牺牲其余部分的形式化推理。
本质上,Lean 的 partial
子集是一种传统的函数式编程语言,但与定理证明功能深度集成;而 unsafe
子集则在少数情形下允许打破 Lean 的运行时不变式,但相应地与定理证明功能的集成程度较低。
类似地,noncomputable
定义可以使用在程序中不合语义、但在逻辑中有意义的特性。
partial
修饰符只能用于函数定义。
偏函数无需展示终止性,Lean 也不会尝试证明它终止。
之所以称为“偏”,是因为它们未必为定义域中的每个元素指定到余域元素的映射:对某些(乃至所有)输入,它们可能无法终止。
这类定义会被繁释为包含显式递归的 预定义 并由内核进行类型检查;不过在逻辑层面它们随后会被当作不透明常量。
函数的返回类型必须是可被占据(inhabited)的;这可确保自洽性。
否则,偏函数就可能拥有诸如 Unit → Empty
的类型。
结合 Empty.elim
,即便该函数并不归约,也可以据此“证明” False
。
对于偏定义,内核负责以下检查:
确认预定义的类型确为一个良构类型;
确认预定义的类型是函数类型;
在“假设 Lean 拥有递归定义”的前提下,检查生成项会通过类型检查。
尽管递归定义不是内核类型论的一部分,仍然可以用内核来检查定义体是否具有正确的类型。 其工作方式与其他函数式语言相同:在一个“该定义已与其类型绑定”的环境中检查定义体,从而为递归的使用做类型检查。 一旦确认通过类型检查,定义体会被丢弃,内核仅保留那个不透明常量。 与所有 Lean 函数一样,编译器会基于繁释得到的 预定义 生成代码。
即便内核不会对偏函数展开,仍可以在不依赖其具体实现的前提下,对调用它们的其他函数开展推理。
递归函数 nextPrime
通过对候选数做试除测试来计算给定数之后的下一个素数,这样的做法效率不高。
由于素数是无限多的,它总是会终止;然而要正式给出这一点的证明并不容易,因此它被标记为 partial
。
def isPrime (n : Nat) : Bool := Id.run do
for i in [2:n] do
if i * i > n then return true
if n % i = 0 then return false
return true
partial def nextPrime (n : Nat) : Nat :=
let n := n + 1
if isPrime n then n else nextPrime n
尽管如此,仍然可以证明下面两个函数是相等的:
def answerUser (n : Nat) : String :=
s!"The next prime is {nextPrime n}"
def answerOtherUser (n : Nat) : String :=
" ".intercalate [
"The",
"next",
"prime",
"is",
toString (nextPrime n)
]
证明包含两步 simp
,用来展示这两个函数在语法上并不相同。
尤其是,字符串插值的反糖导致 answerUser
的结果末尾多了一个 toString ""
:
theorem answer_eq_other : answerUser = answerOtherUser := ⊢ answerUser = answerOtherUser
n:Nat⊢ answerUser n = answerOtherUser n
n:Nat⊢ toString "The next prime is " ++ toString (nextPrime n) ++ toString "" =
" ".intercalate ["The", "next", "prime", "is", toString (nextPrime n)]
n:Nat⊢ "The next prime is " ++ (nextPrime n).repr = " ".intercalate ["The", "next", "prime", "is", (nextPrime n).repr]
All goals completed! 🐙
不安全定义的保障比偏函数更少。 它们的余域不必是可被占据的,且不限于函数定义;同时还能使用一些可能违反内部不变式或破坏抽象的 Lean 特性。 因此,它们完全不能用作数学推理的一部分。
类型论会把偏函数当作不透明常量处理;而不安全定义只能被其他不安全定义引用。 因此,任何调用了不安全函数的函数本身也必须是不安全的;定理则不允许被声明为不安全。
除了不受限制地使用递归之外,不安全函数还能在类型间强制转换、检查两个值是否为内存中的同一对象、读取指针值、以及在原本纯净的代码中运行 IO
动作。
使用这些算子需要对 Lean 的实现有深入理解。
unsafeCast.{u, v} {α : Sort u} {β : Sort v} (a : α) : βunsafeCast.{u, v} {α : Sort u} {β : Sort v} (a : α) : β
This function will cast a value of type α
to type β
, and is a no-op in the
compiler. This function is extremely dangerous because there is no guarantee
that types α
and β
have the same data representation, and this can lead to
memory unsafety. It is also logically unsound, since you could just cast
True
to False
. For all those reasons this function is marked as unsafe
.
It is implemented by lifting both α
and β
into a common universe, and then
using cast (lcProof : ULift (PLift α) = ULift (PLift β))
to actually perform
the cast. All these operations are no-ops in the compiler.
Using this function correctly requires some knowledge of the data representation of the source and target types. Some general classes of casts which are safe in the current runtime:
Array α
to Array β
where α
and β
have compatible representations,
or more generally for other inductive types.
Quot α r
and α
.
@Subtype α p
and α
, or generally any structure containing only one
non-Prop
field of type α
.
Casting α
to/from NonScalar
when α
is a boxed generic type
(i.e. a function that accepts an arbitrary type α
and is not specialized to
a scalar type like UInt8
).
Compares two objects for pointer equality.
Two objects are pointer-equal if, at runtime, they are allocated at exactly the same address. This function is unsafe because it can distinguish between definitionally equal values.
Compares two lists of objects for element-wise pointer equality. Returns true
if both lists are
the same length and the objects at the corresponding indices of each list are pointer-equal.
Two objects are pointer-equal if, at runtime, they are allocated at exactly the same address. This function is unsafe because it can distinguish between definitionally equal values.
Returns the address at which an object is allocated.
This function is unsafe because it can distinguish between definitionally equal values.
Returns true
if a
is an exclusive object.
An object is exclusive if it is single-threaded and its reference counter is 1. This function is unsafe because it can distinguish between definitionally equal values.
Executes arbitrary side effects in a pure context, with exceptions indicated via Except
. This a
dangerous operation that can easily undermine important assumptions about the meaning of Lean
programs, and it should only be used with great care and a thorough understanding of compiler
internals, and even then only to implement observationally pure operations.
This function is not a good way to convert an EIO α
or IO α
into an α
. Instead, use
do
-notation.
Because the resulting value is treated as a side-effect-free term, the compiler may re-order, duplicate, or delete calls to this function. The side effect may even be hoisted into a constant, causing the side effect to occur at initialization time, even if it would otherwise never be called.
Executes arbitrary side effects in a pure context. This a dangerous operation that can easily undermine important assumptions about the meaning of Lean programs, and it should only be used with great care and a thorough understanding of compiler internals, and even then only to implement observationally pure operations.
This function is not a good way to convert a BaseIO α
into an α
. Instead, use
do
-notation.
Because the resulting value is treated as a side-effect-free term, the compiler may re-order, duplicate, or delete calls to this function. The side effect may even be hoisted into a constant, causing the side effect to occur at initialization time, even if it would otherwise never be called.
不安全算子经常被用来利用底层细节编写高性能代码。
类似于通过 FFI 在运行时用 C 代码替换 Lean 代码的方式, 也可以在运行时程序中用不安全 Lean 代码替换安全 Lean 代码。
这可以通过在待替换的函数(通常是 opaque
定义)上添加 implemented_by
属性来实现。
这并不会威胁 Lean 作为逻辑的自洽性:被替换的常量已通过内核检查,而不安全替代仅用于运行时代码。
但这仍然是有风险的——无论是 C 代码还是不安全代码,都可能执行任意副作用。
implemented_by
属性指示编译器在已编译代码中将某个常量替换为另一个常量。
被替换上去的常量可以是不安全的。
attr ::= ... | implemented_by ident
通常,BEq
实例的相等判定需要完全遍历两个参数以判断它们是否相等。
如果它们其实就是内存中的同一个对象,这样的遍历就显得很浪费。
在遍历之前先做一次指针相等性测试,可以尽早捕获这种情况。
比较的类型是 Tree
(二叉树):
inductive Tree α where
| empty
| branch (left : Tree α) (val : α) (right : Tree α)
一个不安全函数可以用指针相等来更快地结束结构相等性测试;当指针不相等时,再回退到结构检查:
unsafe def Tree.fastBEq [BEq α] (t1 t2 : Tree α) : Bool :=
if ptrEq t1 t2 then
true
else
match t1, t2 with
| .empty, .empty => true
| .branch l1 x r1, .branch l2 y r2 =>
if ptrEq x y || x == y then
l1.fastBEq l2 && r1.fastBEq r2
else false
| _, _ => false
在一个不透明定义上添加 implemented_by
属性,就能在安全与不安全代码之间搭桥:
@[implemented_by Tree.fastBEq]
opaque Tree.beq [BEq α] (t1 t2 : Tree α) : Bool
instance [BEq α] : BEq (Tree α) where
beq := Tree.beq
由于 Fin
与其底层的 Nat
具有相同的运行时表示,List.map Fin.val
可以用 unsafeCast
来替换,从而避免一次在实践中“什么也没做”的线性时间遍历:
unsafe def unFinImpl (xs : List (Fin n)) : List Nat :=
unsafeCast xs
@[implemented_by unFinImpl]
def unFin (xs : List (Fin n)) : List Nat :=
xs.map Fin.val
从 Lean 内核的视角看,unFin
是用 List.map
定义的:
theorem unFin_length_eq_length {xs : List (Fin n)} :
(unFin xs).length = xs.length := n:Natxs:List (Fin n)⊢ (unFin xs).length = xs.length
All goals completed! 🐙
在已编译代码中,则不会发生对该列表的遍历。
这种替换方式具有风险:证明与已编译代码之间的一致性完全依赖于两个实现的等价性,而这点无法在 Lean 中证明。 这种一致性依赖 Lean 实现层面的细节。 这些“逃逸舱门”应当非常谨慎地使用。
在检查证明与程序时,Lean 会考虑 可约性(亦称“透明性”)。 某个定义的可约性决定了它在繁释与证明执行过程中会被展开的上下文。
可约性有三个层级:
可约定义基本在各处按需展开。
类型类实例合成、定义相等性检查,以及语言中的其它机制都会将此类定义视为一种近似“缩写”的存在。
Lean.Parser.Command.declaration : command
abbrev
命令即应用了这一设定。
半可约定义不会被潜在昂贵的自动化流程(如类型类实例合成或 simp
)展开,但在进行定义相等性检查或解析广义字段记法时会展开。
Lean.Parser.Command.declaration : command
def
命令通常会创建半可约定义,除非通过属性显式指定了不同的可约性;不过,采用良构递归的定义默认是不可约的。
不可约定义在繁释期间完全不会被展开。
可通过添加 irreducible
属性将某个定义设为不可约。
下面这三个 String
的别名分别是可约、半可约与不可约:
abbrev Phrase := String
def Clause := String
@[irreducible]
def Utterance := String
在繁释器进行定义相等检查时,可约与半可约别名会被展开,从而被视为与 String
等价:
def hello : Phrase := "Hello"
def goodMorning : Clause := "Good morning"
相对地,不可约别名不会在定义相等测试中被展开,因此作为字符串的类型会被拒绝:
def goodEvening : Utterance := "Good evening"
type mismatch "Good evening" has type String : Type but is expected to have type Utterance : Type
由于 Phrase
是可约的,ToString String
实例可被当作 ToString Phrase
实例来用:
#synth ToString Phrase
然而 Clause
是半可约的,因此不能直接使用 ToString String
实例:
#synth ToString Clause
failed to synthesize ToString Clause Additional diagnostic information may be available using the `set_option diagnostics true` command.
可以显式启用该实例:构造一个会化简为 ToString String
实例的 ToString Clause
实例。
该示例之所以可行,是因为在进行定义相等检查时会展开半可约定义:
instance : ToString Clause := inferInstanceAs (ToString String)
在查找匹配名称时,广义字段记法 会展开可约与半可约的声明。
给定 List
的一个半可约别名 Sequence
:
def Sequence := List
def Sequence.ofList (xs : List α) : Sequence α := xs
广义字段记法允许从类型为 Sequence Nat
的项上访问 List.reverse
:
#check let xs : Sequence Nat := .ofList [1,2,3]; xs.reverse
然而,一旦将 Sequence
声明为不可约,就会阻止展开:
attribute [irreducible] Sequence
#check let xs : Sequence Nat := .ofList [1,2,3]; xs.reverse
invalid field 'reverse', the environment does not contain 'Sequence.reverse' xs has type Sequence Nat
可以使用如下三种可约性属性之一来设置某个定义的可约性:
attr ::= ... | reducible
attr ::= ... | semireducible
attr ::= ... | irreducible
这些属性只能在被修改定义所在的同一文件中全局应用;不过,它们也可以在任意位置以 Lean.Parser.Term.attrKind
local
方式应用。
下面这些战术可控制大多数战术会展开哪些定义:with_reducible
、with_reducible_and_instances
与 with_unfolding_all
。
函数 plus
、sum
与 tally
都是 Nat.add
的同义名,且分别为可约、半可约与不可约:
abbrev plus := Nat.add
def sum := Nat.add
@[irreducible]
def tally := Nat.add
可约同义名会被 simp
展开:
theorem plus_eq_add : plus x y = x + y := x:Naty:Nat⊢ plus x y = x + y All goals completed! 🐙
半可约同义名则不会被 simp
展开:
theorem sum_eq_add : sum x y = x + y := x:Naty:Nat⊢ sum x y = x + y x:Naty:Nat⊢ sum x y = x + y
theorem sum_eq_add : sum x y = x + y := x:Naty:Nat⊢ sum x y = x + y All goals completed! 🐙
不可约的 tally
不会被定义相等所化简:
theorem tally_eq_add : tally x y = x + y := x:Naty:Nat⊢ tally x y = x + y x:Naty:Nat⊢ tally x y = x + y
当显式提供时,simp
可以展开任意定义,甚至包括不可约的:
theorem tally_eq_add : tally x y = x + y := x:Naty:Nat⊢ tally x y = x + y All goals completed! 🐙
类似地,可将证明的一部分放入 with_unfolding_all
块中以忽略不可约性:
theorem tally_eq_add : tally x y = x + y := x:Naty:Nat⊢ tally x y = x + y with_unfolding_all All goals completed! 🐙
可以在定义所在的模块中,使用 Lean.Parser.Command.attribute : command
attribute
命令施加相应属性,从而全局修改某个定义的可约性。
在其他模块中,可通过带 local
修饰符的属性应用来修改已导入定义的可约性。
Lean.Parser.commandSeal__ : command
The `seal foo` command ensures that the definition of `foo` is sealed, meaning it is marked as `[irreducible]`.
This command is particularly useful in contexts where you want to prevent the reduction of `foo` in proofs.
In terms of functionality, `seal foo` is equivalent to `attribute [local irreducible] foo`.
This attribute specifies that `foo` should be treated as irreducible only within the local scope,
which helps in maintaining the desired abstraction level without affecting global settings.
seal
与 Lean.Parser.commandUnseal__ : command
The `unseal foo` command ensures that the definition of `foo` is unsealed, meaning it is marked as `[semireducible]`, the
default reducibility setting. This command is useful when you need to allow some level of reduction of `foo` in proofs.
Functionally, `unseal foo` is equivalent to `attribute [local semireducible] foo`.
Applying this attribute makes `foo` semireducible only within the local scope.
unseal
命令是该流程的便捷写法。
The seal foo
command ensures that the definition of foo
is sealed, meaning it is marked as [irreducible]
.
This command is particularly useful in contexts where you want to prevent the reduction of foo
in proofs.
In terms of functionality, seal foo
is equivalent to attribute [local irreducible] foo
.
This attribute specifies that foo
should be treated as irreducible only within the local scope,
which helps in maintaining the desired abstraction level without affecting global settings.
command ::= ...
| The `seal foo` command ensures that the definition of `foo` is sealed, meaning it is marked as `[irreducible]`.
This command is particularly useful in contexts where you want to prevent the reduction of `foo` in proofs.
In terms of functionality, `seal foo` is equivalent to `attribute [local irreducible] foo`.
This attribute specifies that `foo` should be treated as irreducible only within the local scope,
which helps in maintaining the desired abstraction level without affecting global settings.
seal ident ident*
The unseal foo
command ensures that the definition of foo
is unsealed, meaning it is marked as [semireducible]
, the
default reducibility setting. This command is useful when you need to allow some level of reduction of foo
in proofs.
Functionally, unseal foo
is equivalent to attribute [local semireducible] foo
.
Applying this attribute makes foo
semireducible only within the local scope.
command ::= ...
| The `unseal foo` command ensures that the definition of `foo` is unsealed, meaning it is marked as `[semireducible]`, the
default reducibility setting. This command is useful when you need to allow some level of reduction of `foo` in proofs.
Functionally, `unseal foo` is equivalent to `attribute [local semireducible] foo`.
Applying this attribute makes `foo` semireducible only within the local scope.
unseal ident ident*
出于性能考虑,繁释器与许多战术会构建索引与缓存。
其中不少会考虑可约性;而一旦全局改变了可约性,就无法使这些索引/缓存失效并重新生成。
默认情况下,会禁止对可约性进行可能带来不可预测结果的不安全修改;不过,可通过 allowUnsafeReducibility
选项启用之。